diff --git a/backend/plugins/education_api/package.json b/backend/plugins/education_api/package.json new file mode 100644 index 0000000000..7cdad52a23 --- /dev/null +++ b/backend/plugins/education_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "education", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "start": "node -r tsconfig-paths/register dist/src/main.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "erxes-api-shared": "workspace:^" + }, + "devDependencies": {} +} diff --git a/backend/plugins/education_api/project.json b/backend/plugins/education_api/project.json new file mode 100644 index 0000000000..e7a1054c3b --- /dev/null +++ b/backend/plugins/education_api/project.json @@ -0,0 +1,53 @@ +{ + "name": "education_api", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "backend/plugins/education_api/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "nx:run-commands", + "cache": true, + "options": { + "cwd": "backend/plugins/education_api", + "commands": ["pnpm build"] + }, + "dependsOn": ["^build", "build:packageJson"] + }, + + "build:packageJson": { + "executor": "@nx/js:tsc", + "options": { + "main": "backend/plugins/education_api/dist/src/main.js", + "tsConfig": "backend/plugins/education_api/tsconfig.build.json", + "outputPath": "backend/plugins/education_api/dist", + "updateBuildableProjectDepsInPackageJson": true, + + "buildableProjectDepsInPackageJsonType": "dependencies" + } + }, + + "start": { + "executor": "nx:run-commands", + "dependsOn": ["typecheck", "build"], + "options": { + "cwd": "backend/plugins/education_api", + "command": "NODE_ENV=development && node dist/src/main.js" + } + }, + + "serve": { + "executor": "nx:run-commands", + + "options": { + "cwd": "backend/plugins/education_api", + "command": "NODE_ENV=development && pnpm dev" + } + }, + + "docker-build": { + "dependsOn": ["build"], + "command": "docker build -f backend/plugins/education_api/Dockerfile . -t education_api" + } + } +} diff --git a/backend/plugins/education_api/src/apollo/resolvers/index.ts b/backend/plugins/education_api/src/apollo/resolvers/index.ts new file mode 100644 index 0000000000..092740b3a6 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/resolvers/index.ts @@ -0,0 +1,17 @@ +import { apolloCustomScalars } from 'erxes-api-shared/utils'; +import { customResolvers } from './resolvers'; +import { mutations } from './mutations'; +import { queries } from './queries'; + +const resolvers: any = { + Mutation: { + ...mutations, + }, + Query: { + ...queries, + }, + ...apolloCustomScalars, + ...customResolvers, +}; + +export default resolvers; diff --git a/backend/plugins/education_api/src/apollo/resolvers/mutations.ts b/backend/plugins/education_api/src/apollo/resolvers/mutations.ts new file mode 100644 index 0000000000..61a1d6aa89 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/resolvers/mutations.ts @@ -0,0 +1,13 @@ +import { courseMutations } from '@/courses/graphql/resolvers/mutations'; +import { classMutations } from '@/class/graphql/resolvers/mutations'; +import { commentMutations } from '@/comments/graphql/resolvers/mutations'; +import { teacherMutations } from '@/teachers/graphql/resolvers/mutations'; +import { studentMutations } from '@/students/graphql/resolvers/mutations'; + +export const mutations = { + ...courseMutations, + ...classMutations, + ...commentMutations, + ...teacherMutations, + ...studentMutations, +}; diff --git a/backend/plugins/education_api/src/apollo/resolvers/queries.ts b/backend/plugins/education_api/src/apollo/resolvers/queries.ts new file mode 100644 index 0000000000..2d07697a76 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/resolvers/queries.ts @@ -0,0 +1,13 @@ +import { courseQueries } from '@/courses/graphql/resolvers/queries'; +import { classQueries } from '@/class/graphql/resolvers/queries'; +import { commentQueries } from '@/comments/graphql/resolvers/queries'; +import { teacherQueries } from '@/teachers/graphql/resolvers/queries'; +import { studentQueries } from '@/students/graphql/resolvers/queries'; + +export const queries = { + ...courseQueries, + ...classQueries, + ...commentQueries, + ...teacherQueries, + ...studentQueries, +}; diff --git a/backend/plugins/education_api/src/apollo/resolvers/resolvers.ts b/backend/plugins/education_api/src/apollo/resolvers/resolvers.ts new file mode 100644 index 0000000000..ee52ecd1a9 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/resolvers/resolvers.ts @@ -0,0 +1,11 @@ +import courseResolvers from '@/courses/graphql/resolvers/customResolvers'; +import commentResolvers from '@/comments/graphql/resolvers/customResolvers'; +import teacherResolvers from '@/teachers/graphql/resolvers/customResolvers'; +import studentResolvers from '@/students/graphql/resolvers/customResolvers'; + +export const customResolvers = { + ...teacherResolvers, + ...courseResolvers, + ...commentResolvers, + ...studentResolvers, +}; diff --git a/backend/plugins/education_api/src/apollo/schema/schema.ts b/backend/plugins/education_api/src/apollo/schema/schema.ts new file mode 100644 index 0000000000..db00c99b36 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/schema/schema.ts @@ -0,0 +1,61 @@ +import { + mutations as CoursesMutations, + queries as CoursesQueries, + types as CoursesTypes, +} from '@/courses/graphql/schemas/courses'; +import { + mutations as ClassMutations, + queries as ClassQueries, + types as ClassTypes, +} from '@/class/graphql/schemas/class'; +import { + mutations as CommentMutations, + queries as CommentQueries, + types as CommentTypes, +} from '@/comments/graphql/schemas/comments'; +import { + mutations as TeacherMutations, + queries as TeacherQueries, + types as TeacherTypes, +} from '@/teachers/graphql/schemas/teachers'; +import { + mutations as StudentMutations, + queries as StudentQueries, + types as StudentTypes, +} from '@/students/graphql/schemas/students'; + +export const types = ` + enum CacheControlScope { + PUBLIC + PRIVATE + } + + directive @cacheControl( + maxAge: Int + scope: CacheControlScope + inheritMaxAge: Boolean + ) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION + ${CoursesTypes} + ${ClassTypes} + ${CommentTypes} + ${TeacherTypes} + ${StudentTypes} +`; + +export const queries = ` + ${CoursesQueries} + ${ClassQueries} + ${CommentQueries} + ${TeacherQueries} + ${StudentQueries} +`; + +export const mutations = ` + ${CoursesMutations} + ${ClassMutations} + ${CommentMutations} + ${TeacherMutations} + ${StudentMutations} +`; + +export default { types, queries, mutations }; diff --git a/backend/plugins/education_api/src/apollo/typeDefs.ts b/backend/plugins/education_api/src/apollo/typeDefs.ts new file mode 100644 index 0000000000..69a945b637 --- /dev/null +++ b/backend/plugins/education_api/src/apollo/typeDefs.ts @@ -0,0 +1,17 @@ +import { apolloCommonTypes } from 'erxes-api-shared/utils'; +import { DocumentNode } from 'graphql'; +import { gql } from 'graphql-tag'; +import { mutations, queries, types } from '~/apollo/schema/schema'; + +export const typeDefs = async (): Promise => { + return gql(` + ${apolloCommonTypes} + ${types} + extend type Query { + ${queries} + } + extend type Mutation { + ${mutations} + } + `); +}; diff --git a/backend/plugins/education_api/src/connectionResolvers.ts b/backend/plugins/education_api/src/connectionResolvers.ts new file mode 100644 index 0000000000..63bbcb6f02 --- /dev/null +++ b/backend/plugins/education_api/src/connectionResolvers.ts @@ -0,0 +1,71 @@ +import mongoose from 'mongoose'; +import { createGenerateModels } from 'erxes-api-shared/utils'; +import { IMainContext } from 'erxes-api-shared/core-types'; +import { loadCourseClass, ICourseModel } from '@/courses/db/models/Course'; +import { + loadCourseCategoryClass, + ICourseCategoryModel, +} from '@/courses/db/models/Categories'; +import { IClassModel, loadClassesClass } from '@/class/db/models/Classes'; +import { ICommentModel, loadCommentClass } from '@/comments/db/models/Comments'; +import { ITeacherModel, loadTeacherClass } from '@/teachers/db/models/Teachers'; +import { IStudentModel, loadStudentClass } from '@/students/db/models/Students'; +// +import { ICourseDocument } from '@/courses/@types/course'; +import { IClassDocument } from '@/class/@types/classes'; +import { ICourseCategoryDocument } from '@/courses/@types/category'; +import { ICommentDocument } from '@/comments/@types/comments'; +import { ITeacherDocument } from '@/teachers/@types/teachers'; +import { IStudentDocument } from '@/students/@types/students'; + +export interface IModels { + Courses: ICourseModel; + CourseCategories: ICourseCategoryModel; + Classes: IClassModel; + Comments: ICommentModel; + Teachers: ITeacherModel; + Students: IStudentModel; +} + +export interface IContext extends IMainContext { + commonQuerySelector: any; + models: IModels; +} + +export const loadClasses = (db: mongoose.Connection): IModels => { + const models = {} as IModels; + + models.Courses = db.model( + 'courses', + loadCourseClass(models), + ); + + models.CourseCategories = db.model< + ICourseCategoryDocument, + ICourseCategoryModel + >('course_categories', loadCourseCategoryClass(models)); + + models.Classes = db.model( + 'course_classes', + loadClassesClass(models), + ); + + models.Comments = db.model( + 'course_comments', + loadCommentClass(models), + ); + + models.Teachers = db.model( + 'course_teachers', + loadTeacherClass(models), + ); + + models.Students = db.model( + 'course_students', + loadStudentClass(models), + ); + + return models; +}; + +export const generateModels = createGenerateModels(loadClasses); diff --git a/backend/plugins/education_api/src/main.ts b/backend/plugins/education_api/src/main.ts new file mode 100644 index 0000000000..bea59cfb38 --- /dev/null +++ b/backend/plugins/education_api/src/main.ts @@ -0,0 +1,32 @@ +import { startPlugin } from 'erxes-api-shared/utils'; +import resolvers from './apollo/resolvers'; +import { generateModels } from './connectionResolvers'; +import { typeDefs } from '~/apollo/typeDefs'; +import { appRouter } from './trpc/init-trpc'; + +startPlugin({ + name: 'education', + port: 3322, + graphql: async () => ({ + typeDefs: await typeDefs(), + resolvers, + }), + apolloServerContext: async (subdomain, context) => { + const models = await generateModels(subdomain); + + context.models = models; + + return context; + }, + + trpcAppRouter: { + router: appRouter, + createContext: async (subdomain, context) => { + const models = await generateModels(subdomain); + + context.models = models; + + return context; + }, + }, +}); diff --git a/backend/plugins/education_api/src/modules/class/@types/classes.ts b/backend/plugins/education_api/src/modules/class/@types/classes.ts new file mode 100644 index 0000000000..296f6844a5 --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/@types/classes.ts @@ -0,0 +1,25 @@ +import { + ICursorPaginateParams, + IListParams, +} from 'erxes-api-shared/core-types'; +import { Document } from 'mongoose'; + +export interface IClass { + name: string; + description: string; + location: string; + level: string; +} + +export interface IClassParams extends IListParams, ICursorPaginateParams { + name: string; + description: string; + location: string; + level: string; +} + +export interface IClassDocument extends IClass, Document { + _id: string; + createdAt?: Date; + updatedAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/class/constants.ts b/backend/plugins/education_api/src/modules/class/constants.ts new file mode 100644 index 0000000000..7fd79ae964 --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/constants.ts @@ -0,0 +1,6 @@ +export const CLASS_LEVEL_TYPES = { + Beginner: 'Beginner', + Intermediate: 'Intermediate', + Advanced: 'Advanced', + ALL: ['Beginner', 'Intermediate', 'Advanced'], +}; diff --git a/backend/plugins/education_api/src/modules/class/db/definitions/class.ts b/backend/plugins/education_api/src/modules/class/db/definitions/class.ts new file mode 100644 index 0000000000..08a1b9a360 --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/db/definitions/class.ts @@ -0,0 +1,24 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { CLASS_LEVEL_TYPES } from '../../constants'; + +export const classSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, required: true, label: 'Name' }, + description: { type: String, required: true, label: 'Description' }, + location: { type: String, required: true, label: 'Location' }, + level: { + type: String, + enum: CLASS_LEVEL_TYPES.ALL, + default: CLASS_LEVEL_TYPES.Beginner, + label: 'Level', + }, + createdAt: { type: Date, default: new Date(), label: 'Created at' }, + updatedAt: { type: Date, default: new Date(), label: 'Updated at' }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/class/db/models/Classes.ts b/backend/plugins/education_api/src/modules/class/db/models/Classes.ts new file mode 100644 index 0000000000..1cf9cab62c --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/db/models/Classes.ts @@ -0,0 +1,49 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IClass, IClassDocument } from '~/modules/class/@types/classes'; +import { classSchema } from '~/modules/class/db/definitions/class'; + +export interface IClassModel extends Model { + createClass(doc: IClass): Promise; + getClass(_id: string): Promise; + updateClass(_id: string, doc: IClass): Promise; + removeClasses(classIds: string[]): Promise; +} + +export const loadClassesClass = (models: IModels) => { + class Class { + public static async getClass(_id: string) { + const courseClass = await models.Classes.findOne({ _id }); + + if (!courseClass) { + throw new Error('Class not found'); + } + + return courseClass; + } + + public static async createClass(doc) { + return await models.Classes.create({ + ...doc, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + public static async updateClass(_id, doc) { + await models.Classes.updateOne( + { _id }, + { $set: { ...doc, updatedAt: new Date() } }, + ); + + return models.Classes.findOne({ _id }); + } + + public static async removeClasses(classIds) { + return models.Classes.deleteMany({ _id: { $in: classIds } }); + } + } + classSchema.loadClass(Class); + + return classSchema; +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/Classes.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/Classes.ts new file mode 100644 index 0000000000..899a2eec9f --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/Classes.ts @@ -0,0 +1,4 @@ +import { IContext } from '~/connectionResolvers'; +import { ICourseDocument } from '~/modules/courses/@types/course'; + +export default {}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/index.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..5879ea0fdf --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,5 @@ +import Classes from './Classes'; + +export default { + Classes, +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/class.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/class.ts new file mode 100644 index 0000000000..003ef0a42d --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/class.ts @@ -0,0 +1,28 @@ +import { IContext } from '~/connectionResolvers'; +import { IClass } from '@/class/@types/classes'; + +export const classMutations = { + classAdd: async (_root, doc: IClass, { models }: IContext) => { + const courseClass = await models.Classes.createClass(doc); + + return courseClass; + }, + /** + * + */ + classEdit: async ( + _root, + { _id, ...doc }: { _id: string } & IClass, + { models }: IContext, + ) => { + return await models.Classes.updateClass(_id, doc); + }, + // + classesRemove: async ( + _root, + { classIds }: { classIds: string[] }, + { models }: IContext, + ) => { + return await models.Classes.removeClasses(classIds); + }, +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/index.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..887c93ed5a --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/mutations/index.ts @@ -0,0 +1,5 @@ +import { classMutations as classMainMutations } from './class'; + +export const classMutations = { + ...classMainMutations, +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/class.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/class.ts new file mode 100644 index 0000000000..bb363f3669 --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/class.ts @@ -0,0 +1,22 @@ +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IClassParams } from '~/modules/class/@types/classes'; + +export const classQueries = { + courseClasses: async ( + _root: undefined, + params: IClassParams, + { models }: IContext, + ) => { + const { list, totalCount, pageInfo } = await cursorPaginate({ + model: models.Classes, + params, + query: {}, + }); + + return { list, totalCount, pageInfo }; + }, + classDetail: async (_root, { _id }, { models }: IContext) => { + return models.Classes.getClass(_id); + }, +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/index.ts b/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..d25912cd56 --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/resolvers/queries/index.ts @@ -0,0 +1,5 @@ +import { classQueries as classMainQueries } from './class'; + +export const classQueries = { + ...classMainQueries, +}; diff --git a/backend/plugins/education_api/src/modules/class/graphql/schemas/class.ts b/backend/plugins/education_api/src/modules/class/graphql/schemas/class.ts new file mode 100644 index 0000000000..b47243ec7a --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/schemas/class.ts @@ -0,0 +1,34 @@ +export const types = ` + type Classes { + _id: String + name: String + description: String + location: String + level: String + createdAt: Date + updatedAt: Date + } + + type ClassesResponse { + list: [Classes] + totalCount: Int + pageInfo: PageInfo + } +`; + +export const queries = ` + courseClasses(page: Int, perPage: Int, limit: Int, cursor: String, direction: CURSOR_DIRECTION): ClassesResponse +`; + +const classesCommonParams = ` + name : String + description : String + location : String + level: String, +`; + +export const mutations = ` + classAdd(${classesCommonParams}): Classes + classEdit(_id:String!, ${classesCommonParams}): Classes + classesRemove(classIds: [String]): JSON +`; diff --git a/backend/plugins/education_api/src/modules/class/graphql/schemas/index.ts b/backend/plugins/education_api/src/modules/class/graphql/schemas/index.ts new file mode 100644 index 0000000000..662a4b4a3e --- /dev/null +++ b/backend/plugins/education_api/src/modules/class/graphql/schemas/index.ts @@ -0,0 +1,17 @@ +import { + mutations as ClassMutations, + queries as ClassQueries, + types as ClassTypes, +} from './class'; + +export const types = ` + ${ClassTypes} +`; + +export const queries = ` + ${ClassQueries} +`; + +export const mutations = ` + ${ClassMutations} +`; diff --git a/backend/plugins/education_api/src/modules/comments/@types/comments.ts b/backend/plugins/education_api/src/modules/comments/@types/comments.ts new file mode 100644 index 0000000000..7c4ab7575f --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/@types/comments.ts @@ -0,0 +1,24 @@ +import { + ICursorPaginateParams, + IListParams, +} from 'erxes-api-shared/core-types'; +import { Document } from 'mongoose'; + +export interface IComment { + type: string; + courseId: string; + content: string; + parentId?: string; +} + +export interface ICommentParams extends IListParams, ICursorPaginateParams { + type: string; + content: string; + parentId?: string; +} + +export interface ICommentDocument extends IComment, Document { + _id: string; + createdAt?: Date; + updatedAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/comments/db/definitions/comments.ts b/backend/plugins/education_api/src/modules/comments/db/definitions/comments.ts new file mode 100644 index 0000000000..3bfbbe44e1 --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/db/definitions/comments.ts @@ -0,0 +1,16 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; + +export const commentSchema = new Schema( + { + _id: mongooseStringRandomId, + courseId: { type: String, label: 'Course Id' }, + content: { type: String, label: 'Content' }, + parentId: { type: String, label: 'Parent Id' }, + createdAt: { type: Date, label: 'Created at' }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/comments/db/models/Comments.ts b/backend/plugins/education_api/src/modules/comments/db/models/Comments.ts new file mode 100644 index 0000000000..fc2070ec54 --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/db/models/Comments.ts @@ -0,0 +1,43 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IComment, ICommentDocument } from '@/comments/@types/comments'; +import { commentSchema } from '@/comments/db/definitions/comments'; + +export interface ICommentModel extends Model { + getComment(_id: string): Promise; + createComment(doc: IComment): Promise; + updateComment(_id: string, doc: IComment): Promise; + deleteComment(_id: string): void; +} + +export const loadCommentClass = (models: IModels) => { + class Comment { + /** + * Retreives comment + */ + public static async getComment(_id: string) { + const comment = await models.Comments.findOne({ _id }); + + if (!comment) { + throw new Error('Comment not found'); + } + + return comment; + } + + public static async createComment(doc: ICommentDocument) { + return models.Comments.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async deleteComment(_id: string) { + return models.Comments.deleteOne({ _id }); + } + } + + commentSchema.loadClass(Comment); + + return commentSchema; +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/comments.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/comments.ts new file mode 100644 index 0000000000..95c8a8372d --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/comments.ts @@ -0,0 +1,10 @@ +import { IContext } from '~/connectionResolvers'; +import { ICommentDocument } from '@/comments/@types/comments'; + +export default { + childCount: async (comment: ICommentDocument, _, { models }: IContext) => { + return models.Comments.find({ + parentId: comment._id, + }).countDocuments(); + }, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/index.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..7c99563ac8 --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,5 @@ +import Comment from './comments'; + +export default { + Comment, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/comments.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/comments.ts new file mode 100644 index 0000000000..70811e28de --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/comments.ts @@ -0,0 +1,24 @@ +import { IContext } from '~/connectionResolvers'; +import { IComment } from '@/comments/@types/comments'; + +export const commentMutations = { + courseCommentAdd: async (_root, doc: IComment, { models }: IContext) => { + return await models.Comments.createComment(doc); + }, + courseCommentEdit: async ( + _root, + { _id, ...doc }: { _id: string } & IComment, + { models }: IContext, + ) => { + return await models.Comments.updateComment(_id, doc); + }, + courseCommentRemove: async ( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) => { + await models.Comments.deleteComment(_id); + + return _id; + }, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/index.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..80544b88be --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/mutations/index.ts @@ -0,0 +1,5 @@ +import { commentMutations as commentMainMutations } from './comments'; + +export const commentMutations = { + ...commentMainMutations, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/comments.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/comments.ts new file mode 100644 index 0000000000..33c62d3b9a --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/comments.ts @@ -0,0 +1,22 @@ +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { ICommentParams } from '@/comments/@types/comments'; + +export const commentQueries = { + courseComments: async ( + _root: undefined, + params: ICommentParams, + { models }: IContext, + ) => { + const { list, totalCount, pageInfo } = await cursorPaginate({ + model: models.Comments, + params, + query: {}, + }); + + return { list, totalCount, pageInfo }; + }, + courseCommentCount: async (_root, { _id }, { models }: IContext) => { + return models.Comments.find({ courseId: _id }).countDocuments(); + }, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/index.ts b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..da9c708937 --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/resolvers/queries/index.ts @@ -0,0 +1,5 @@ +import { commentQueries as commentMainQueries } from './comments'; + +export const commentQueries = { + ...commentMainQueries, +}; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/schemas/comments.ts b/backend/plugins/education_api/src/modules/comments/graphql/schemas/comments.ts new file mode 100644 index 0000000000..3314fe8822 --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/schemas/comments.ts @@ -0,0 +1,43 @@ +export const types = ` + type Comment { + _id: String + courseId: String + parentId: String + content: String + childCount: Int + createdAt: Date + updatedAt: Date + } + + type CommentResponse { + list: [Comment] + totalCount: Int + pageInfo: PageInfo + } +`; + +const queryParams = ` + courseId: String!, + parentId: String, + page: Int, + perPage: Int, + cursor: String, + direction: CURSOR_DIRECTION +`; + +export const queries = ` + courseComments(${queryParams}): CommentResponse + courseCommentCount(courseId: String!): Int +`; + +const mutationParams = ` + courseId: String! + content: String! + parentId: String +`; + +export const mutations = ` + courseCommentAdd(${mutationParams}): Comment + courseCommentEdit(_id: String, ${mutationParams}): Comment + courseCommentRemove(_id: String!): JSON +`; diff --git a/backend/plugins/education_api/src/modules/comments/graphql/schemas/index.ts b/backend/plugins/education_api/src/modules/comments/graphql/schemas/index.ts new file mode 100644 index 0000000000..53fa54d85b --- /dev/null +++ b/backend/plugins/education_api/src/modules/comments/graphql/schemas/index.ts @@ -0,0 +1,17 @@ +import { + mutations as CommentMutations, + queries as CommentQueries, + types as CommentTypes, +} from './comments'; + +export const types = ` + ${CommentTypes} +`; + +export const queries = ` + ${CommentQueries} +`; + +export const mutations = ` + ${CommentMutations} +`; diff --git a/backend/plugins/education_api/src/modules/courses/@types/category.ts b/backend/plugins/education_api/src/modules/courses/@types/category.ts new file mode 100644 index 0000000000..b73f50ca89 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/@types/category.ts @@ -0,0 +1,37 @@ +import { + ICursorPaginateParams, + IListParams, +} from 'erxes-api-shared/core-types'; +export interface ICourseCategory { + _id: string; + name: string; + description?: string; + parentId?: string; + code: string; + isRoot?: boolean; + activityCount?: number; + attachment?: any; + order: string; +} + +export interface ICourseCategoryParams + extends IListParams, + ICursorPaginateParams { + name: string; + code?: string; + categoryId: string; + description?: string; + createdAt?: Date; + type?: string; + attachment?: any; + status?: string; + startDate: Date; + endDate?: Date; + deadline?: Date; + unitPrice: number; +} + +export interface ICourseCategoryDocument extends ICourseCategory, Document { + _id: string; + createdAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/courses/@types/course.ts b/backend/plugins/education_api/src/modules/courses/@types/course.ts new file mode 100644 index 0000000000..0504501630 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/@types/course.ts @@ -0,0 +1,43 @@ +import { + ICursorPaginateParams, + IListParams, +} from 'erxes-api-shared/core-types'; +import { Document } from 'mongoose'; + +export interface ICourse { + name: string; + categoryId: string; + description?: string; + createdAt?: Date; + type: string; + attachment?: any; + status?: string; + startDate: Date; + endDate?: Date; + deadline?: Date; + unitPrice: number; + limit: number; + classId: string; + location?: string; +} + +export interface ICourseParams extends IListParams, ICursorPaginateParams { + name: string; + categoryId: string; + description?: string; + createdAt?: Date; + type?: string; + attachment?: any; + status?: string; + startDate: Date; + endDate?: Date; + deadline?: Date; + unitPrice: number; + classId: string; + location?: string; +} + +export interface ICourseDocument extends ICourse, Document { + createdAt: Date; + modifiedAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/courses/constants.ts b/backend/plugins/education_api/src/modules/courses/constants.ts new file mode 100644 index 0000000000..0dff9c4360 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/constants.ts @@ -0,0 +1,14 @@ +export const COURSE_STATUSES = { + ACTIVE: 'active', + DRAFT: 'draft', + ARCHIVED: 'archived', + ALL: ['active', 'draft', 'archived'], +}; + +export const COURSE_TYPES = { + Training: 'Training', + Event: 'Event', + Volunteering: 'Volunteering', + MENTORSHIP: 'Mentorship', + ALL: ['Training', 'Event', 'Volunteering', 'Mentorship'], +}; diff --git a/backend/plugins/education_api/src/modules/courses/db/definitions/category.ts b/backend/plugins/education_api/src/modules/courses/db/definitions/category.ts new file mode 100644 index 0000000000..f976e9a628 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/db/definitions/category.ts @@ -0,0 +1,28 @@ +import { Schema } from 'mongoose'; +import { attachmentSchema } from 'erxes-api-shared/core-modules'; +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; + +export const courseCategorySchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + code: { type: String, unique: true, label: 'Code' }, + order: { type: String, label: 'Order' }, + parentId: { type: String, optional: true, label: 'Parent ID' }, + description: { type: String, optional: true, label: 'Description' }, + isRoot: { type: Boolean, optional: true, label: 'Is Root' }, + activityCount: { + type: Number, + optional: true, + label: 'Activity Count', + }, + attachment: { + type: attachmentSchema, + optional: true, + label: 'Image', + }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/courses/db/definitions/course.ts b/backend/plugins/education_api/src/modules/courses/db/definitions/course.ts new file mode 100644 index 0000000000..54f5897dbc --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/db/definitions/course.ts @@ -0,0 +1,44 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { COURSE_STATUSES, COURSE_TYPES } from '@/courses/constants'; +import { attachmentSchema } from 'erxes-api-shared/core-modules'; + +export const courseSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + categoryId: { type: String, optional: true, label: 'Category ID' }, + classId: { type: String, optional: true, label: 'Class ID' }, + category: { type: Object, optional: true, label: 'Category' }, + description: { type: String, label: 'Description' }, + createdAt: { type: Date, default: new Date(), label: 'Created At' }, + type: { + type: String, + enum: COURSE_TYPES.ALL, + default: COURSE_TYPES.Training, + label: 'Type', + }, + attachment: { + type: attachmentSchema, + }, + + searchText: { type: String, optional: true, index: true }, + status: { + type: String, + enum: COURSE_STATUSES.ALL, + default: COURSE_STATUSES.DRAFT, + optional: true, + label: 'Status', + }, + startDate: { type: Date, label: 'Start Date' }, + endDate: { type: Date, label: 'End Date' }, + deadline: { type: Date, label: 'Use Finsh Date' }, + unitPrice: { type: Number, optional: true, label: 'Unit price' }, + limit: { type: Number, label: 'Limit of students' }, + location: { type: Date, label: 'Location' }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/courses/db/models/Categories.ts b/backend/plugins/education_api/src/modules/courses/db/models/Categories.ts new file mode 100644 index 0000000000..617e3e3799 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/db/models/Categories.ts @@ -0,0 +1,117 @@ +import { Model } from 'mongoose'; +import { + ICourseCategory, + ICourseCategoryDocument, +} from '@/courses/@types/category'; +import { IModels } from '~/connectionResolvers'; +import { courseCategorySchema } from '@/courses/db/definitions/category'; + +export interface ICourseCategoryModel extends Model { + getCourseCategory(selector: any): Promise; + createCourseCategory(doc: ICourseCategory): Promise; + updateCourseCategory( + _id: string, + doc: ICourseCategory, + ): Promise; + removeCourseCategory(_id: string): Promise; + generateOrder( + parentCategory: any, + doc: ICourseCategory, + ): Promise; +} + +export const loadCourseCategoryClass = (models: IModels) => { + class CourseCategory { + public static async getCourseCategory(selector: any) { + const courseCategory = await models.CourseCategories.findOne(selector); + + if (!courseCategory) { + throw new Error('Course category not found'); + } + + return courseCategory; + } + public static async createCourseCategory(doc) { + const parentCategory = doc.parentId + ? await models.CourseCategories.findOne({ + _id: doc.parentId, + }).lean() + : undefined; + + // Generating order + doc.order = await this.generateOrder(parentCategory, doc); + + return models.CourseCategories.create(doc); + } + + public static async updateCourseCategory(_id, doc) { + const parentCategory = doc.parentId + ? await models.CourseCategories.findOne({ + _id: doc.parentId, + }).lean() + : undefined; + + if (parentCategory && parentCategory.parentId === _id) { + throw new Error('Cannot change category'); + } + + // Generatingg order + doc.order = await this.generateOrder(parentCategory, doc); + + const courseCategory = await models.CourseCategories.getCourseCategory({ + _id, + }); + + const childCategories = await models.CourseCategories.find({ + $and: [ + { order: { $regex: new RegExp(courseCategory.order, 'i') } }, + { _id: { $ne: _id } }, + ], + }); + + await models.CourseCategories.updateOne({ _id }, { $set: doc }); + + // updating child categories order + childCategories.forEach(async (category) => { + let { order } = category; + + order = order.replace(courseCategory.order, doc.order); + + await models.CourseCategories.updateOne( + { _id: category._id }, + { $set: { order } }, + ); + }); + + return models.CourseCategories.findOne({ _id }); + } + + public static async removeCourseCategory(_id) { + await models.CourseCategories.getCourseCategory({ _id }); + + let count = await models.Courses.countDocuments({ categoryId: _id }); + + count += await models.CourseCategories.countDocuments({ + parentId: _id, + }); + + if (count > 0) { + throw new Error("Can't remove a course category"); + } + + return models.CourseCategories.deleteOne({ _id }); + } + + public static async generateOrder(parentCategory, doc) { + const order = parentCategory + ? `${parentCategory.order}/${doc.code}` + : `${doc.code}`; + + return order; + } + } + + courseCategorySchema.loadClass(CourseCategory); + + return courseCategorySchema; +}; diff --git a/backend/plugins/education_api/src/modules/courses/db/models/Course.ts b/backend/plugins/education_api/src/modules/courses/db/models/Course.ts new file mode 100644 index 0000000000..19695402d0 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/db/models/Course.ts @@ -0,0 +1,69 @@ +import { Model } from 'mongoose'; +import { ICourse, ICourseDocument } from '@/courses/@types/course'; +import { courseSchema } from '@/courses/db/definitions/course'; +import { IModels } from '~/connectionResolvers'; +import { validSearchText } from 'erxes-api-shared/utils'; + +export interface ICourseModel extends Model { + createCourse(doc: ICourse): Promise; + getCourse(_id: string): Promise; + updateCourse(_id: string, doc: ICourse): Promise; + removeCourses(courseIds: string[]): Promise; +} + +export const loadCourseClass = (models: IModels) => { + class Course { + public static fillSearchText(doc: ICourse) { + return validSearchText([doc.name || '', doc.description || '']); + } + /** + * Retreives course + */ + public static async getCourse(_id: string) { + const course = await models.Courses.findOne({ _id }); + + if (!course) { + throw new Error('Course not found'); + } + + return course; + } + + /** + * Create a course + */ + public static async createCourse(doc) { + return await models.Courses.create({ + ...doc, + createdAt: new Date(), + modifiedAt: new Date(), + searchText: this.fillSearchText(doc), + }); + } + + /** + * Remove course + */ + public static async removeCourses(courseIds) { + return models.Courses.deleteMany({ _id: { $in: courseIds } }); + } + + /** + * Update course + */ + public static async updateCourse(_id, doc) { + const searchText = this.fillSearchText( + Object.assign(await models.Courses.getCourse(_id), doc), + ); + await models.Courses.updateOne( + { _id }, + { $set: { ...doc, searchText, modifiedAt: new Date() } }, + ); + + return models.Courses.findOne({ _id }); + } + } + courseSchema.loadClass(Course); + + return courseSchema; +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/Course.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/Course.ts new file mode 100644 index 0000000000..5833b6dbca --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/Course.ts @@ -0,0 +1,15 @@ +import { IContext } from '~/connectionResolvers'; +import { ICourseDocument } from '@/courses/@types/course'; + +export default { + category: async (course: ICourseDocument, _, { models }: IContext) => { + return models.CourseCategories.findOne({ + _id: course?.categoryId, + }); + }, + class: async (course: ICourseDocument, _, { models }: IContext) => { + return models.Classes.findOne({ + _id: course?.classId, + }); + }, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/category.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/category.ts new file mode 100644 index 0000000000..f725b0fd54 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/category.ts @@ -0,0 +1,28 @@ +import { escapeRegExp } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { ICourseCategoryDocument } from '@/courses/@types/category'; + +export default { + __resolveReference: async ( + { _id }: { _id: string }, + { models }: IContext, + ) => { + return models.CourseCategories.findOne({ _id }); + }, + isRoot: (category: ICourseCategoryDocument) => { + return !category.parentId; + }, + courseCount: async ( + category: ICourseCategoryDocument, + _args: undefined, + { models }: IContext, + ) => { + const course_category_ids = await models.CourseCategories.find( + { order: { $regex: new RegExp(`^${escapeRegExp(category.order)}`) } }, + { _id: 1 }, + ); + return models.Courses.countDocuments({ + categoryId: { $in: course_category_ids }, + }); + }, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/index.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..e864c75e87 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,7 @@ +import Course from './course'; +import CourseCategory from './category'; + +export default { + Course, + CourseCategory, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/course.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/course.ts new file mode 100644 index 0000000000..51ef77ef4a --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/course.ts @@ -0,0 +1,82 @@ +import { IContext } from '~/connectionResolvers'; +import { ICourseCategory } from '@/courses/@types/category'; +import { ICourse } from '@/courses/@types/course'; + +export const courseMutations = { + /** + * Creates a new course + */ + courseAdd: async (_root, doc: ICourse, { models }: IContext) => { + return await models.Courses.createCourse(doc); + }, + /** + * Edit a course + */ + courseEdit: async ( + _root, + { _id, ...doc }: { _id: string } & ICourse, + { models }: IContext, + ) => { + return await models.Courses.updateCourse(_id, doc); + }, + /** + * Removes course + */ + courseRemove: async ( + _root, + { courseIds }: { courseIds: string[] }, + { models }: IContext, + ) => { + await models.Courses.removeCourses(courseIds); + + return courseIds; + }, + /** + * Change a status of course + */ + changeCourseStatus: async ( + _root, + { _id, status }: { _id: string; status: string }, + { models }: IContext, + ) => { + const updated = await models.Courses.findOneAndUpdate( + { _id }, + { $set: { status } }, + { new: true }, + ); + return updated; + }, + /** + * Create a course category + */ + courseCategoryAdd: async ( + _root, + doc: ICourseCategory, + { models }: IContext, + ) => { + return await models.CourseCategories.createCourseCategory(doc); + }, + /** + * Edits a course category + */ + courseCategoryEdit: async ( + _root, + { _id, ...doc }: { _id: string } & ICourseCategory, + { models }: IContext, + ) => { + return await models.CourseCategories.updateCourseCategory( + _id, + doc as ICourseCategory, + ); + }, + /** + * Delete a course category + */ + courseCategoryRemove: async ( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) => { + return await models.CourseCategories.removeCourseCategory(_id); + }, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/index.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..b3cd7cac0c --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/mutations/index.ts @@ -0,0 +1,5 @@ +import { courseMutations as courseMainMutations } from './course'; + +export const courseMutations = { + ...courseMainMutations, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/course.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/course.ts new file mode 100644 index 0000000000..55827a8a2f --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/course.ts @@ -0,0 +1,84 @@ +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext, IModels } from '~/connectionResolvers'; +import { ICourseParams } from '~/modules/courses/@types/course'; + +const generateFilter = async ( + models: IModels, + commonQuerySelector: any, + params, +) => { + const filter: any = commonQuerySelector; + + if (params.categoryId) { + filter.categoryId = params.categoryId; + } + + if (params.searchValue) { + filter.searchText = { $in: [new RegExp(`.*${params.searchValue}.*`, 'i')] }; + } + + if (params.statuses) { + filter.status = { $in: params.statuses }; + } + + if (params.ids) { + filter._id = { $in: params.ids }; + } + + return filter; +}; + +export const sortBuilder = (params) => { + const sortField = params.sortField; + const sortDirection = params.sortDirection || 0; + + if (sortField) { + return { [sortField]: sortDirection }; + } + + return {}; +}; + +export const courseQueries = { + courses: async ( + _root: undefined, + params: ICourseParams, + { commonQuerySelector, models }: IContext, + ) => { + const filter = await generateFilter(models, commonQuerySelector, params); + + const { list, totalCount, pageInfo } = await cursorPaginate({ + model: models.Courses, + params, + query: filter, + }); + + return { list, totalCount, pageInfo }; + }, + + courseDetail: async (_root, { _id }, { models }: IContext) => { + return models.Courses.getCourse(_id); + }, + + courseCategories: async ( + _root, + { parentId, searchValue }, + { commonQuerySelector, models }: IContext, + ) => { + const filter: any = commonQuerySelector; + + if (parentId) { + filter.parentId = parentId; + } + + if (searchValue) { + filter.name = new RegExp(`.*${searchValue}.*`, 'i'); + } + + return models.CourseCategories.find(filter).sort({ order: 1 }); + }, + + activityCategoriesTotalCount: async (_root, _param, { models }: IContext) => { + return models.CourseCategories.find().countDocuments(); + }, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/index.ts b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..4c9e39fc9e --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/resolvers/queries/index.ts @@ -0,0 +1,5 @@ +import { courseQueries as courseMainQueries } from './course'; + +export const courseQueries = { + ...courseMainQueries, +}; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/schemas/courses.ts b/backend/plugins/education_api/src/modules/courses/graphql/schemas/courses.ts new file mode 100644 index 0000000000..be50daafd7 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/schemas/courses.ts @@ -0,0 +1,105 @@ +export const types = ` + extend type User @key(fields: "_id") { + _id: String! @external + } + type Course { + _id: String! + name: String + categoryId: String + classId: String + category : CourseCategory + class: Classes + description: String + createdAt: Date + type: String + attachment: Attachment + status: String + startDate: Date, + endDate: Date, + deadline: Date, + unitPrice: Float, + commentCount: Int + primaryTeacher: User + teachers : [User] + limit : Int + location: String + } + + type CourseListResponse { + list: [Course], + totalCount: Float, + pageInfo: PageInfo + } + + type CourseCategory { + _id: String! + name: String + description: String + parentId: String + code: String! + order: String! + isRoot: Boolean + courseCount: Int + attachment: Attachment + } + enum StatusType { + active + draft + } +`; + +const courseCategoryParams = ` + name: String!, + code: String!, + description: String, + parentId: String, + attachment: AttachmentInput, +`; + +const queryParams = ` + page: Int + perPage: Int + ids: [String] + searchValue: String + sortField: String + sortDirection: Int + categoryId: String + statuses : [String] + limit: Int + cursor: String + direction: CURSOR_DIRECTION +`; + +export const queries = ` + courses(${queryParams}): CourseListResponse + courseDetail(_id: String!): Course + courseCategories(parentId: String, searchValue: String): [CourseCategory] + courseCategoriesTotalCount: Int +`; + +const mutationParams = ` + name: String!, + type: String!, + categoryId: String!, + description: String, + attachment: AttachmentInput, + startDate: Date!, + endDate: Date, + deadline: Date, + unitPrice: Float!, + status: String + limit : Int + classId: String! + location: String +`; + +export const mutations = ` + courseAdd(${mutationParams}): Course + courseEdit(_id:String!, ${mutationParams}): Course + courseRemove(courseIds: [String]): JSON + changeCourseStatus(_id:String!, status : StatusType): Course + + courseCategoryAdd(${courseCategoryParams}): CourseCategory + courseCategoryEdit(_id: String!, ${courseCategoryParams}): CourseCategory + courseCategoryRemove(_id: String!): JSON +`; diff --git a/backend/plugins/education_api/src/modules/courses/graphql/schemas/index.ts b/backend/plugins/education_api/src/modules/courses/graphql/schemas/index.ts new file mode 100644 index 0000000000..7f0b28dbf1 --- /dev/null +++ b/backend/plugins/education_api/src/modules/courses/graphql/schemas/index.ts @@ -0,0 +1,17 @@ +import { + mutations as CoursesMutations, + queries as CoursesQueries, + types as CoursesTypes, +} from './courses'; + +export const types = ` + ${CoursesTypes} +`; + +export const queries = ` + ${CoursesQueries} +`; + +export const mutations = ` + ${CoursesMutations} +`; diff --git a/backend/plugins/education_api/src/modules/students/@types/students.ts b/backend/plugins/education_api/src/modules/students/@types/students.ts new file mode 100644 index 0000000000..2e5875ee5b --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/@types/students.ts @@ -0,0 +1,26 @@ +import { + ICursorPaginateParams, + IDetail, + ILink, + IListParams, +} from 'erxes-api-shared/core-types'; +import { Document } from 'mongoose'; + +export interface IStudent { + username?: string; + password: string; + email?: string; + details?: IDetail; + links?: ILink; + isActive?: boolean; + deviceTokens?: string[]; +} + +export interface IStudentParams extends IListParams, ICursorPaginateParams { + userId?: string; +} + +export interface IStudentDocument extends IStudent, Document { + createdAt?: Date; + updatedAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/students/db/definitions/students.ts b/backend/plugins/education_api/src/modules/students/db/definitions/students.ts new file mode 100644 index 0000000000..4795613dfd --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/db/definitions/students.ts @@ -0,0 +1,80 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; + +const detailSchema = new Schema( + { + avatar: { type: String, label: 'Avatar' }, + coverPhoto: { type: String, label: 'Cover photo' }, + shortName: { + type: String, + optional: true, + label: 'Short name', + }, + fullName: { type: String, label: 'Full name' }, + birthDate: { type: Date, label: 'Birth date' }, + workStartedDate: { + type: Date, + label: 'Date to joined to work', + }, + position: { type: String, label: 'Position' }, + location: { + type: String, + optional: true, + label: 'Location', + }, + description: { + type: String, + optional: true, + label: 'Description', + }, + operatorPhone: { + type: String, + optional: true, + label: 'Operator phone', + }, + firstName: { type: String, label: 'First name' }, + middleName: { type: String, label: 'Middle name' }, + lastName: { type: String, label: 'Last name' }, + }, + { _id: false }, +); + +export const studentSchema = new Schema( + { + _id: mongooseStringRandomId, + username: { type: String, label: 'Username' }, + password: { type: String, optional: true }, + email: { + type: String, + unique: true, + /** + * RFC 5322 compliant regex. Taken from http://emailregex.com/ + */ + match: [ + /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})|([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,})$/, + 'Please fill a valid email address', + ], + label: 'Email', + }, + details: { + type: detailSchema, + default: {}, + label: 'Details', + }, + links: { type: Object, default: {}, label: 'Links' }, + isActive: { + type: Boolean, + default: true, + label: 'Is active', + }, + deviceTokens: { + type: [String], + default: [], + label: 'Device tokens', + }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/students/db/models/Students.ts b/backend/plugins/education_api/src/modules/students/db/models/Students.ts new file mode 100644 index 0000000000..10f9deb26b --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/db/models/Students.ts @@ -0,0 +1,39 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IStudent, IStudentDocument } from '@/students/@types/students'; +import { studentSchema } from '@/students/db/definitions/students'; + +export interface IStudentModel extends Model { + getStudent(_id: string): Promise; + createStudent(doc: IStudent): Promise; + deleteStudent(_id: string): void; +} + +export const loadStudentClass = (models: IModels) => { + class Student { + public static async getStudent(_id: string) { + const student = await models.Students.findOne({ _id }); + + if (!student) { + throw new Error('Student not found'); + } + + return student; + } + + public static async createStudent(doc: IStudentDocument) { + return models.Students.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async deleteStudent(_id: string) { + return models.Students.deleteOne({ _id }); + } + } + + studentSchema.loadClass(Student); + + return studentSchema; +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/index.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..46f356e6b4 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,5 @@ +import Student from './students'; + +export default { + Student, +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/students.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/students.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/customResolvers/students.ts @@ -0,0 +1 @@ +export default {}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/index.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..2173f092f4 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/index.ts @@ -0,0 +1,5 @@ +import { studentMutations as studentMainMutations } from './students'; + +export const studentMutations = { + ...studentMainMutations, +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/students.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/students.ts new file mode 100644 index 0000000000..6d7a41b0f7 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/mutations/students.ts @@ -0,0 +1,13 @@ +import { IContext } from '~/connectionResolvers'; + +export const studentMutations = { + studentRemove: async ( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) => { + await models.Students.deleteStudent(_id); + + return _id; + }, +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/index.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..c4e8796dc2 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/index.ts @@ -0,0 +1,5 @@ +import { studentQueries as studentMainQueries } from './students'; + +export const studentQueries = { + ...studentMainQueries, +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/students.ts b/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/students.ts new file mode 100644 index 0000000000..d166ab0d2b --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/resolvers/queries/students.ts @@ -0,0 +1,25 @@ +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; + +type IListArgs = { + sortDirection?: number; + sortField?: string; + searchValue?: string; + isActive?: boolean; +}; + +export const studentQueries = { + students: async ( + _root: undefined, + params: IListArgs, + { models }: IContext, + ) => { + const { list, totalCount, pageInfo } = await cursorPaginate({ + model: models.Students, + params, + query: {}, + }); + + return { list, totalCount, pageInfo }; + }, +}; diff --git a/backend/plugins/education_api/src/modules/students/graphql/schemas/index.ts b/backend/plugins/education_api/src/modules/students/graphql/schemas/index.ts new file mode 100644 index 0000000000..1aee6dae82 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/schemas/index.ts @@ -0,0 +1,17 @@ +import { + mutations as StudentMutations, + queries as StudentQueries, + types as StudentTypes, +} from './students'; + +export const types = ` + ${StudentTypes} +`; + +export const queries = ` + ${StudentQueries} +`; + +export const mutations = ` + ${StudentMutations} +`; diff --git a/backend/plugins/education_api/src/modules/students/graphql/schemas/students.ts b/backend/plugins/education_api/src/modules/students/graphql/schemas/students.ts new file mode 100644 index 0000000000..2acd393ba5 --- /dev/null +++ b/backend/plugins/education_api/src/modules/students/graphql/schemas/students.ts @@ -0,0 +1,54 @@ +const commonDetailFields = ` + avatar: String + coverPhoto: String + fullName: String + shortName: String + birthDate: Date + position: String + workStartedDate: Date + location: String + description: String + operatorPhone: String + firstName: String + middleName: String + lastName: String + employeeId: String +`; + +export const types = ` + type StudentDetailsType { + ${commonDetailFields} + } + + type Student { + _id: String + username: String + email: String + details: StudentDetailsType + links: JSON + isActive: Boolean + cursor: String + } + + type StudentListResponse { + list: [Student] + totalCount: Int + pageInfo: PageInfo + } +`; + +const queryParams = ` + page: Int, + perPage: Int, + cursor: String, + direction: CURSOR_DIRECTION + searchValue: String, +`; + +export const queries = ` + students(${queryParams}): StudentListResponse +`; + +export const mutations = ` + studentRemove(_id: String!): JSON +`; diff --git a/backend/plugins/education_api/src/modules/teachers/@types/teachers.ts b/backend/plugins/education_api/src/modules/teachers/@types/teachers.ts new file mode 100644 index 0000000000..4d53c999ac --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/@types/teachers.ts @@ -0,0 +1,18 @@ +import { + ICursorPaginateParams, + IListParams, +} from 'erxes-api-shared/core-types'; +import { Document } from 'mongoose'; + +export interface ITeacher { + userId: string; +} + +export interface ITeacherParams extends IListParams, ICursorPaginateParams { + userId?: string; +} + +export interface ITeacherDocument extends ITeacher, Document { + createdAt?: Date; + updatedAt: Date; +} diff --git a/backend/plugins/education_api/src/modules/teachers/db/definitions/teachers.ts b/backend/plugins/education_api/src/modules/teachers/db/definitions/teachers.ts new file mode 100644 index 0000000000..1faa076281 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/db/definitions/teachers.ts @@ -0,0 +1,14 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; + +export const teacherSchema = new Schema( + { + _id: mongooseStringRandomId, + userId: { type: String, label: 'User Id' }, + createdAt: { type: Date, label: 'Created at' }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/education_api/src/modules/teachers/db/models/Teachers.ts b/backend/plugins/education_api/src/modules/teachers/db/models/Teachers.ts new file mode 100644 index 0000000000..9e5d1063c0 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/db/models/Teachers.ts @@ -0,0 +1,39 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { ITeacher, ITeacherDocument } from '@/teachers/@types/teachers'; +import { teacherSchema } from '@/teachers/db/definitions/teachers'; + +export interface ITeacherModel extends Model { + getTeacher(_id: string): Promise; + createTeacher(doc: ITeacher): Promise; + deleteTeacher(_id: string): void; +} + +export const loadTeacherClass = (models: IModels) => { + class Teacher { + public static async getTeacher(_id: string) { + const teacher = await models.Teachers.findOne({ _id }); + + if (!teacher) { + throw new Error('Teacher not found'); + } + + return teacher; + } + + public static async createTeacher(doc: ITeacherDocument) { + return models.Teachers.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async deleteTeacher(_id: string) { + return models.Teachers.deleteOne({ _id }); + } + } + + teacherSchema.loadClass(Teacher); + + return teacherSchema; +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/index.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..2a8c58b4ed --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,5 @@ +import Teacher from './teachers'; + +export default { + Teacher, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/teachers.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/teachers.ts new file mode 100644 index 0000000000..f472bbfc3a --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/customResolvers/teachers.ts @@ -0,0 +1,7 @@ +import { ITeacherDocument } from '@/teachers/@types/teachers'; + +export default { + user(teacher: ITeacherDocument) { + return teacher.userId && { __typename: 'User', _id: teacher.userId }; + }, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/index.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..0c503c6390 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/index.ts @@ -0,0 +1,5 @@ +import { teacherMutations as teacherMainMutations } from './teachers'; + +export const teacherMutations = { + ...teacherMainMutations, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/teachers.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/teachers.ts new file mode 100644 index 0000000000..3e48d2fc9d --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/mutations/teachers.ts @@ -0,0 +1,17 @@ +import { IContext } from '~/connectionResolvers'; +import { ITeacher } from '@/teachers/@types/teachers'; + +export const teacherMutations = { + teacherAdd: async (_root, doc: ITeacher, { models }: IContext) => { + return await models.Teachers.createTeacher(doc); + }, + teacherRemove: async ( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) => { + await models.Teachers.deleteTeacher(_id); + + return _id; + }, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/index.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..1146c1a8c8 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/index.ts @@ -0,0 +1,5 @@ +import { teacherQueries as teacherMainQueries } from './teachers'; + +export const teacherQueries = { + ...teacherMainQueries, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/teachers.ts b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/teachers.ts new file mode 100644 index 0000000000..7467165712 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/resolvers/queries/teachers.ts @@ -0,0 +1,19 @@ +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { ITeacherParams } from '~/modules/teachers/@types/teachers'; + +export const teacherQueries = { + teachers: async ( + _root: undefined, + params: ITeacherParams, + { models }: IContext, + ) => { + const { list, totalCount, pageInfo } = await cursorPaginate({ + model: models.Teachers, + params, + query: {}, + }); + + return { list, totalCount, pageInfo }; + }, +}; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/schemas/index.ts b/backend/plugins/education_api/src/modules/teachers/graphql/schemas/index.ts new file mode 100644 index 0000000000..4323b8cc57 --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/schemas/index.ts @@ -0,0 +1,17 @@ +import { + mutations as TeacherMutations, + queries as TeacherQueries, + types as TeacherTypes, +} from './teachers'; + +export const types = ` + ${TeacherTypes} +`; + +export const queries = ` + ${TeacherQueries} +`; + +export const mutations = ` + ${TeacherMutations} +`; diff --git a/backend/plugins/education_api/src/modules/teachers/graphql/schemas/teachers.ts b/backend/plugins/education_api/src/modules/teachers/graphql/schemas/teachers.ts new file mode 100644 index 0000000000..1a16eeb30e --- /dev/null +++ b/backend/plugins/education_api/src/modules/teachers/graphql/schemas/teachers.ts @@ -0,0 +1,33 @@ +export const types = ` + type Teacher { + _id: String + userId: String + user: User + } + + type TeacherResponse { + list: [Teacher] + totalCount: Int + pageInfo: PageInfo + } +`; + +const queryParams = ` + page: Int, + perPage: Int, + cursor: String, + direction: CURSOR_DIRECTION +`; + +export const queries = ` + teachers(${queryParams}): TeacherResponse +`; + +const mutationParams = ` + userId: String, +`; + +export const mutations = ` + teacherAdd(${mutationParams}): Teacher + teacherRemove(_id: String!): JSON +`; diff --git a/backend/plugins/education_api/src/trpc/init-trpc.ts b/backend/plugins/education_api/src/trpc/init-trpc.ts new file mode 100644 index 0000000000..4d802c0d09 --- /dev/null +++ b/backend/plugins/education_api/src/trpc/init-trpc.ts @@ -0,0 +1,31 @@ +import { initTRPC } from '@trpc/server'; +import * as trpcExpress from '@trpc/server/adapters/express'; + +import { getSubdomain } from 'erxes-api-shared/utils'; +import { generateModels } from '~/connectionResolvers'; + +export const createContext = async ({ + req, +}: trpcExpress.CreateExpressContextOptions) => { + const subdomain = getSubdomain(req); + const models = await generateModels(subdomain); + + return { + subdomain, + models, + }; +}; + +export type ITRPCContext = Awaited>; + +const t = initTRPC.context().create(); + +export const appRouter = t.router({ + education: { + hello: t.procedure.query(() => { + return 'Hello Sample'; + }), + }, +}); + +export type AppRouter = typeof appRouter; diff --git a/backend/plugins/education_api/src/trpc/trpc-clients.ts b/backend/plugins/education_api/src/trpc/trpc-clients.ts new file mode 100644 index 0000000000..6ebd627b51 --- /dev/null +++ b/backend/plugins/education_api/src/trpc/trpc-clients.ts @@ -0,0 +1,20 @@ +import { httpBatchLink, createTRPCUntypedClient } from '@trpc/client'; +import { getPlugin, isEnabled } from 'erxes-api-shared/utils'; + +export const coreTRPCClient = async (): Promise< + ReturnType +> => { + const isCoreEnabled = await isEnabled('core'); + + if (!isCoreEnabled) { + throw new Error('Core plugin is not enabled'); + } + + const core = await getPlugin('core'); + + const client = createTRPCUntypedClient({ + links: [httpBatchLink({ url: core.address + '/trpc' })], + }); + + return client; +}; diff --git a/backend/plugins/education_api/tsconfig.build.json b/backend/plugins/education_api/tsconfig.build.json new file mode 100644 index 0000000000..550e12343a --- /dev/null +++ b/backend/plugins/education_api/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "paths": { + "~/*": ["./src/*"], + "@/*": ["./src/modules/*"] + }, + "types": ["node"] + }, + "exclude": ["node_modules", "dist", "**/*spec.ts"] +} diff --git a/backend/plugins/education_api/tsconfig.json b/backend/plugins/education_api/tsconfig.json new file mode 100644 index 0000000000..a88e987219 --- /dev/null +++ b/backend/plugins/education_api/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "esModuleInterop": true, + "target": "es2017", + "sourceMap": true, + "inlineSources": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "resolveJsonModule": true, + "types": ["jest", "node"], + "paths": { + "~/*": ["./src/*"], + "@/*": ["./src/modules/*"], + "erxes-api-shared/*": ["../../erxes-api-shared/src/*"] + } + }, + "ts-node": { + "files": true, + "require": ["tsconfig-paths/register"] + }, + "exclude": ["dist", "frontend/**/*"], + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/frontend/core-ui/src/i18n/translations/en.json b/frontend/core-ui/src/i18n/translations/en.json index 5c37b3a3b3..51428003a2 100644 --- a/frontend/core-ui/src/i18n/translations/en.json +++ b/frontend/core-ui/src/i18n/translations/en.json @@ -36,7 +36,13 @@ }, "sample": "Sample", "modules": "Modules", +<<<<<<< HEAD + "education": "Education", + "courses": "Course", + "classes": "Class" +======= "pms": "PMS", "tms": "TMS" +>>>>>>> 8d39ca2d6403a0973bae87aba078551a5118e8de } } diff --git a/frontend/core-ui/src/i18n/translations/mn.json b/frontend/core-ui/src/i18n/translations/mn.json index ce46aff8f9..c1aed01638 100644 --- a/frontend/core-ui/src/i18n/translations/mn.json +++ b/frontend/core-ui/src/i18n/translations/mn.json @@ -11,6 +11,9 @@ "main": "Баримтууд", "records": "Бичилтүүд", "odds": "Зөрүүтэй баримтууд" - } + }, + "education": "Боловсрол", + "courses": "Хөтөлбөр", + "classes": "Анги" } -} \ No newline at end of file +} diff --git a/frontend/plugins/education_ui/eslint.config.js b/frontend/plugins/education_ui/eslint.config.js new file mode 100644 index 0000000000..2016babea6 --- /dev/null +++ b/frontend/plugins/education_ui/eslint.config.js @@ -0,0 +1,12 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/plugins/education_ui/jest.config.ts b/frontend/plugins/education_ui/jest.config.ts new file mode 100644 index 0000000000..38ec0f1923 --- /dev/null +++ b/frontend/plugins/education_ui/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'education_ui', + preset: '../../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/frontend/plugins/education_ui', +}; diff --git a/frontend/plugins/education_ui/module-federation.config.ts b/frontend/plugins/education_ui/module-federation.config.ts new file mode 100644 index 0000000000..5ac0aa3462 --- /dev/null +++ b/frontend/plugins/education_ui/module-federation.config.ts @@ -0,0 +1,37 @@ +import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + +const coreLibraries = new Set([ + 'react', + 'react-dom', + 'react-router', + 'react-router-dom', + 'erxes-ui', + '@apollo/client', + 'jotai', + 'ui-modules', + 'react-i18next', +]); + +const config: ModuleFederationConfig = { + name: 'education_ui', + exposes: { + './config': './src/config.ts', + './courses': './src/modules/courses/Main.tsx', + './classes': './src/modules/classes/Main.tsx', + './teachers': './src/modules/teachers/Main.tsx', + }, + + shared: (libraryName, defaultConfig) => { + if (coreLibraries.has(libraryName)) { + return defaultConfig; + } + + // Returning false means the library is not shared. + return false; + }, +}; + +/** + * Nx requires a default export of the config to allow correct resolution of the module federation graph. + **/ +export default config; diff --git a/frontend/plugins/education_ui/project.json b/frontend/plugins/education_ui/project.json new file mode 100644 index 0000000000..e2771dca06 --- /dev/null +++ b/frontend/plugins/education_ui/project.json @@ -0,0 +1,61 @@ +{ + "name": "education_ui", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/plugins/education_ui/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "@nx/rspack:rspack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "target": "web", + "outputPath": "dist/frontend/plugins/education_ui", + "main": "frontend/plugins/education_ui/src/main.ts", + "tsConfig": "frontend/plugins/education_ui/tsconfig.app.json", + "rspackConfig": "frontend/plugins/education_ui/rspack.config.ts", + "assets": [] + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production", + "optimization": true, + "sourceMap": false, + "rspackConfig": "frontend/plugins/education_ui/rspack.config.prod.ts" + } + } + }, + "serve": { + "executor": "@nx/rspack:module-federation-dev-server", + "options": { + "buildTarget": "education_ui:build:development", + "port": 3022 + }, + "configurations": { + "development": {}, + "production": { + "buildTarget": "education_ui:build:production" + } + } + }, + "serve-static": { + "executor": "@nx/rspack:module-federation-static-server", + "defaultConfiguration": "production", + "options": { + "serveTarget": "education_ui:serve" + }, + "configurations": { + "development": { + "serveTarget": "education_ui:serve:development" + }, + "production": { + "serveTarget": "education_ui:serve:production" + } + } + } + } +} diff --git a/frontend/plugins/education_ui/rspack.config.prod.ts b/frontend/plugins/education_ui/rspack.config.prod.ts new file mode 100644 index 0000000000..0c2be66b3a --- /dev/null +++ b/frontend/plugins/education_ui/rspack.config.prod.ts @@ -0,0 +1 @@ +export default require('./rspack.config'); diff --git a/frontend/plugins/education_ui/rspack.config.ts b/frontend/plugins/education_ui/rspack.config.ts new file mode 100644 index 0000000000..b819ec5333 --- /dev/null +++ b/frontend/plugins/education_ui/rspack.config.ts @@ -0,0 +1,20 @@ +import { composePlugins, withNx, withReact } from '@nx/rspack'; +import { withModuleFederation } from '@nx/rspack/module-federation'; + +import baseConfig from './module-federation.config'; + +const config = { + ...baseConfig, +}; + +// Nx plugins for rspack to build config object from Nx options and context. +/** + * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation + * The DTS Plugin can be enabled by setting dts: true + * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html + */ +export default composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }), +); diff --git a/frontend/plugins/education_ui/src/assets/fb.svg b/frontend/plugins/education_ui/src/assets/fb.svg new file mode 100644 index 0000000000..19b0276a06 --- /dev/null +++ b/frontend/plugins/education_ui/src/assets/fb.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/plugins/education_ui/src/assets/ig.svg b/frontend/plugins/education_ui/src/assets/ig.svg new file mode 100644 index 0000000000..f24f18b796 --- /dev/null +++ b/frontend/plugins/education_ui/src/assets/ig.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/plugins/education_ui/src/assets/messenger.svg b/frontend/plugins/education_ui/src/assets/messenger.svg new file mode 100644 index 0000000000..064cb205ae --- /dev/null +++ b/frontend/plugins/education_ui/src/assets/messenger.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/plugins/education_ui/src/bootstrap.tsx b/frontend/plugins/education_ui/src/bootstrap.tsx new file mode 100644 index 0000000000..32482de416 --- /dev/null +++ b/frontend/plugins/education_ui/src/bootstrap.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import * as ReactDOM from 'react-dom/client'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement, +); +root.render( + +
App
+
, +); diff --git a/frontend/plugins/education_ui/src/config.ts b/frontend/plugins/education_ui/src/config.ts new file mode 100644 index 0000000000..8e7030ce94 --- /dev/null +++ b/frontend/plugins/education_ui/src/config.ts @@ -0,0 +1,54 @@ +import { + IconBook, + IconBooks, + IconCategoryFilled, + IconMan, + IconSchool, + IconUser, +} from '@tabler/icons-react'; +import { IUIConfig } from 'erxes-ui/types'; + +export const CONFIG: IUIConfig = { + name: 'education', + icon: IconBook, + modules: [ + { + name: 'courses', + icon: IconBook, + path: 'courses', + hasSettings: false, + hasWidgets: false, + submenus: [ + { + name: 'category', + path: 'courses/course-category', + icon: IconCategoryFilled, + }, + { + name: 'students', + path: 'courses/students', + icon: IconMan, + }, + { + name: 'parents', + path: 'courses/parents', + icon: IconUser, + }, + ], + }, + { + name: 'classes', + icon: IconBooks, + path: 'classes', + hasSettings: false, + hasWidgets: false, + }, + { + name: 'teachers', + icon: IconSchool, + path: 'teachers', + hasSettings: false, + hasWidgets: false, + }, + ], +}; diff --git a/frontend/plugins/education_ui/src/index.html b/frontend/plugins/education_ui/src/index.html new file mode 100644 index 0000000000..f54bbf0247 --- /dev/null +++ b/frontend/plugins/education_ui/src/index.html @@ -0,0 +1,14 @@ + + + + + Sample + + + + + + +
+ + diff --git a/frontend/plugins/education_ui/src/main.ts b/frontend/plugins/education_ui/src/main.ts new file mode 100644 index 0000000000..b93c7a0268 --- /dev/null +++ b/frontend/plugins/education_ui/src/main.ts @@ -0,0 +1 @@ +import('./bootstrap'); diff --git a/frontend/plugins/education_ui/src/modules/classes/Main.tsx b/frontend/plugins/education_ui/src/modules/classes/Main.tsx new file mode 100644 index 0000000000..37d9206754 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/Main.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router'; + +const Class = lazy(() => + import('~/pages/ClassIndexPage').then((module) => ({ + default: module.default, + })), +); + +const CourseClassMain = () => { + return ( + }> + + } /> + + + ); +}; + +export default CourseClassMain; diff --git a/frontend/plugins/education_ui/src/modules/classes/add-class/AddClassForm.tsx b/frontend/plugins/education_ui/src/modules/classes/add-class/AddClassForm.tsx new file mode 100644 index 0000000000..d0b70c29d4 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/add-class/AddClassForm.tsx @@ -0,0 +1,61 @@ +import { useForm } from 'react-hook-form'; + +import { zodResolver } from '@hookform/resolvers/zod'; + +import { Button, Form, useToast, Dialog } from 'erxes-ui'; +import { useAddClass } from '@/classes/hooks/useAddClass'; +import { ApolloError } from '@apollo/client'; +import { ClassAddCoreFields } from '@/classes/add-class/components/ClassAddCoreFields'; +import { + classFormSchema, + ClassFormType, +} from '@/classes/add-class/components/formSchema'; + +export function AddClassForm({ + onOpenChange, +}: { + onOpenChange: (open: boolean) => void; +}) { + const { classAdd } = useAddClass(); + const form = useForm({ + resolver: zodResolver(classFormSchema), + }); + const { toast } = useToast(); + const onSubmit = (data: ClassFormType) => { + classAdd({ + variables: data, + onError: (e: ApolloError) => { + console.log(e.message); + toast({ + title: 'Error', + description: e.message, + }); + }, + onCompleted: () => { + form.reset(); + onOpenChange(false); + }, + }); + }; + + return ( +
+ + + + + + + + + + + ); +} diff --git a/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddCoreFields.tsx b/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddCoreFields.tsx new file mode 100644 index 0000000000..ade548cdea --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddCoreFields.tsx @@ -0,0 +1,93 @@ +import { UseFormReturn } from 'react-hook-form'; +import { Form, Input, Select } from 'erxes-ui'; +import { CLASS_LEVEL_OPTIONS } from '@/classes/constants/ClassConstants'; +import { ClassFormType } from '@/classes/add-class/components/formSchema'; + +export const ClassAddCoreFields = ({ + form, +}: { + form: UseFormReturn; +}) => { + return ( + <> + ( + + NAME + + + + + + + )} + /> + ( + + DESCRIPTION + + + + + + + )} + /> + ( + + LOCATION + + + + + + + )} + /> + ( + + LEVEL + + + + )} + /> + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddSheet.tsx b/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddSheet.tsx new file mode 100644 index 0000000000..d7c2caf69d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/add-class/components/ClassAddSheet.tsx @@ -0,0 +1,54 @@ +import { IconPlus } from '@tabler/icons-react'; + +import { + Button, + Dialog, + Kbd, + usePreviousHotkeyScope, + useScopedHotkeys, + useSetHotkeyScope, +} from 'erxes-ui'; +import { useState } from 'react'; +import { AddClassForm } from '@/classes/add-class/AddClassForm'; +import { ClassHotKeyScope } from '@/classes/types/ClassHotKeyScope'; + +export const ClassAddSheet = () => { + const setHotkeyScope = useSetHotkeyScope(); + const [open, setOpen] = useState(false); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const onOpen = () => { + setOpen(true); + setHotkeyScopeAndMemorizePreviousScope(ClassHotKeyScope.ClassAddSheet); + }; + + const onClose = () => { + setOpen(false); + setHotkeyScope('class-page'); + }; + + useScopedHotkeys(`c`, () => onOpen(), 'class-page'); + useScopedHotkeys(`esc`, () => onClose(), ClassHotKeyScope.ClassAddSheet); + + return ( + + + + + { + e.preventDefault(); + }} + > + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/add-class/components/formSchema.ts b/frontend/plugins/education_ui/src/modules/classes/add-class/components/formSchema.ts new file mode 100644 index 0000000000..60bc80e3aa --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/add-class/components/formSchema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const classFormSchema = z.object({ + name: z + .string() + .min(2, 'Name must be at least 2 characters') + .max(100, 'Name must be less than 100 characters'), + description: z.string(), + location: z.string(), + level: z.enum(['Beginner', 'Intermediate', 'Advanced']), +}); + +export type ClassFormType = z.infer; diff --git a/frontend/plugins/education_ui/src/modules/classes/components/ClassCommandBar.tsx b/frontend/plugins/education_ui/src/modules/classes/components/ClassCommandBar.tsx new file mode 100644 index 0000000000..3b1ba16973 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/components/ClassCommandBar.tsx @@ -0,0 +1,27 @@ +import { IconCopy, IconTrash } from '@tabler/icons-react'; + +import { Button, CommandBar, RecordTable, Separator } from 'erxes-ui'; + +export const ClassCommandBar = () => { + const { table } = RecordTable.useRecordTable(); + + return ( + 0}> + + + {table.getFilteredSelectedRowModel().rows.length} selected + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/components/ClassMoreColumn.tsx b/frontend/plugins/education_ui/src/modules/classes/components/ClassMoreColumn.tsx new file mode 100644 index 0000000000..ccd44a2626 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/components/ClassMoreColumn.tsx @@ -0,0 +1,33 @@ +import { Cell } from '@tanstack/react-table'; +import { RecordTable, usePreviousHotkeyScope } from 'erxes-ui'; +import { useSetAtom } from 'jotai'; +import { useQueryState } from 'erxes-ui'; +import { renderingClassDetailAtom } from '@/classes/states/classDetailStates'; +import { ClassHotKeyScope } from '@/classes/types/ClassHotKeyScope'; + +export const ClassMoreColumnCell = ({ cell }: { cell: Cell }) => { + const [, setOpen] = useQueryState('classId'); + const setRenderingContactDetail = useSetAtom(renderingClassDetailAtom); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + const { _id } = cell.row.original; + return ( + { + setOpen(_id); + setTimeout(() => { + setHotkeyScopeAndMemorizePreviousScope( + ClassHotKeyScope.ClassEditSheet, + ); + }, 100); + setRenderingContactDetail(false); + }} + /> + ); +}; + +export const classMoreColumn = { + id: 'more', + cell: ClassMoreColumnCell, + size: 33, +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/components/ClassesColumns.tsx b/frontend/plugins/education_ui/src/modules/classes/components/ClassesColumns.tsx new file mode 100644 index 0000000000..cf223420ee --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/components/ClassesColumns.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import type { Cell, ColumnDef } from '@tanstack/react-table'; + +import { RecordTableInlineHead } from 'erxes-ui/modules/record-table/components/RecordTableInlineHead'; +import { + RecordTableInlineCell, + RecordTableInlineCellEditForm, +} from 'erxes-ui/modules/record-table/record-table-cell/components/RecordTableInlineCell'; +import { TextFieldInput } from 'erxes-ui/modules/record-field/meta-inputs/components/TextFieldInput'; +import { RecordTable } from 'erxes-ui'; +import { classMoreColumn } from './ClassMoreColumn'; +import { IClass } from '@/classes/types/type'; + +const TableTextInput = ({ cell }: { cell: Cell }) => { + const [value, setValue] = useState(cell.getValue() as string); + return ( + { + console.group('+'); + }} + getValue={() => cell.getValue()} + value={value} + display={() => value} + edit={() => ( + + setValue(e.target.value)} + /> + + )} + /> + ); +}; + +const checkBoxColumn = RecordTable.checkboxColumn as ColumnDef; + +export const classColumns: ColumnDef[] = [ + classMoreColumn as ColumnDef, + checkBoxColumn, + { + id: 'name', + accessorKey: 'name', + header: () => , + cell: ({ cell }) => , + }, + { + id: 'description', + accessorKey: 'description', + header: () => , + cell: ({ cell }) => , + }, + { + id: 'location', + accessorKey: 'location', + header: () => , + cell: ({ cell }) => , + }, + { + id: 'level', + accessorKey: 'level', + header: () => , + cell: ({ cell }) => , + }, +]; diff --git a/frontend/plugins/education_ui/src/modules/classes/components/ClassesHeader.tsx b/frontend/plugins/education_ui/src/modules/classes/components/ClassesHeader.tsx new file mode 100644 index 0000000000..463de0900d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/components/ClassesHeader.tsx @@ -0,0 +1,30 @@ +import { IconSchool } from '@tabler/icons-react'; +import { Breadcrumb, Button, Separator } from 'erxes-ui'; +import { Link } from 'react-router'; +import { ClassAddSheet } from '@/classes/add-class/components/ClassAddSheet'; +import { PageHeader } from 'ui-modules'; + +export const ClassesHeader = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/components/ClassesRecordTable.tsx b/frontend/plugins/education_ui/src/modules/classes/components/ClassesRecordTable.tsx new file mode 100644 index 0000000000..6ecb4e600f --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/components/ClassesRecordTable.tsx @@ -0,0 +1,41 @@ +import { RecordTable } from 'erxes-ui'; +import { useClasses } from '@/classes/hooks/useClasses'; +import { classColumns } from '@/classes/components/ClassesColumns'; +import { ClassCommandBar } from '@/classes/components/ClassCommandBar'; + +export const ClassesRecordTable = () => { + const { classes, handleFetchMore, loading, pageInfo } = useClasses({}); + + const { hasPreviousPage, hasNextPage } = pageInfo || {}; + + return ( + + + + + + + {loading && } + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/constants/ClassConstants.tsx b/frontend/plugins/education_ui/src/modules/classes/constants/ClassConstants.tsx new file mode 100644 index 0000000000..873c9e8890 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/constants/ClassConstants.tsx @@ -0,0 +1,5 @@ +export const CLASS_LEVEL_OPTIONS = [ + { label: 'Анхан шат', value: 'Beginner' }, + { label: 'Дунд шат', value: 'Intermediate' }, + { label: 'Гүнзгий шат', value: 'Advanced' }, +]; diff --git a/frontend/plugins/education_ui/src/modules/classes/graphql/mutations/addClass.ts b/frontend/plugins/education_ui/src/modules/classes/graphql/mutations/addClass.ts new file mode 100644 index 0000000000..cac780f44c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/graphql/mutations/addClass.ts @@ -0,0 +1,24 @@ +import { gql } from '@apollo/client'; + +export const ADD_CLASS = gql` + mutation classAdd( + $level: String! + $name: String + $description: String + $location: String + ) { + classAdd( + level: $level + name: $name + description: $description + location: $location + ) { + _id + description + location + level + createdAt + updatedAt + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/classes/graphql/queries/getClasses.tsx b/frontend/plugins/education_ui/src/modules/classes/graphql/queries/getClasses.tsx new file mode 100644 index 0000000000..6c893024ce --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/graphql/queries/getClasses.tsx @@ -0,0 +1,34 @@ +import { gql } from '@apollo/client'; + +export const GET_CLASSES = gql` + query Classes( + $page: Int + $perPage: Int + $direction: CURSOR_DIRECTION + $cursor: String + ) { + courseClasses( + page: $page + perPage: $perPage + direction: $direction + cursor: $cursor + ) { + list { + _id + name + description + location + level + createdAt + updatedAt + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/classes/hooks/useAddClass.tsx b/frontend/plugins/education_ui/src/modules/classes/hooks/useAddClass.tsx new file mode 100644 index 0000000000..a827dd4a32 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/hooks/useAddClass.tsx @@ -0,0 +1,58 @@ +import { useMutation, ApolloCache, MutationHookOptions } from '@apollo/client'; +import { IClass } from '@/classes/types/type'; +import { ADD_CLASS } from '@/classes/graphql/mutations/addClass'; +import { GET_CLASSES } from '@/classes/graphql/queries/getClasses'; +import { useRecordTableCursor } from 'erxes-ui/modules'; + +interface ClassData { + courseClasses: { + list: IClass[]; + totalCount: number; + }; +} + +interface AddClassResult { + classAdd: IClass; +} + +export function useAddClass( + options?: MutationHookOptions, +) { + const { cursor } = useRecordTableCursor({ + sessionKey: 'class_cursor', + }); + + const [classAdd, { loading, error }] = useMutation( + ADD_CLASS, + { + ...options, + update: (cache: ApolloCache, { data }) => { + try { + const queryVariables = { perPage: 30, cursor }; + const existingData = cache.readQuery({ + query: GET_CLASSES, + variables: queryVariables, + }); + if (!existingData || !existingData.courseClasses || !data?.classAdd) + return; + + cache.writeQuery({ + query: GET_CLASSES, + variables: queryVariables, + data: { + courseClasses: { + ...existingData.courseClasses, + list: [data.classAdd, ...existingData.courseClasses.list], + totalCount: existingData.courseClasses.totalCount + 1, + }, + }, + }); + } catch (e) { + console.error('error:', e); + } + }, + }, + ); + + return { classAdd, loading, error }; +} diff --git a/frontend/plugins/education_ui/src/modules/classes/hooks/useClasses.tsx b/frontend/plugins/education_ui/src/modules/classes/hooks/useClasses.tsx new file mode 100644 index 0000000000..55d47e8de7 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/hooks/useClasses.tsx @@ -0,0 +1,69 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { IClass } from '@/classes/types/type'; +import { + useRecordTableCursor, + IRecordTableCursorPageInfo, + mergeCursorData, + EnumCursorDirection, + validateFetchMore, +} from 'erxes-ui'; +import { GET_CLASSES } from '@/classes/graphql/queries/getClasses'; + +export const CLASSES_PER_PAGE = 30; + +export const useClasses = (options?: QueryHookOptions) => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'class_cursor', + }); + + const { data, loading, error, fetchMore } = useQuery<{ + courseClasses: { + list: IClass[]; + pageInfo: IRecordTableCursorPageInfo; + }; + }>(GET_CLASSES, { + ...options, + variables: { + perPage: CLASSES_PER_PAGE, + cursor, + }, + }); + + const { list: classes, pageInfo } = data?.courseClasses || {}; + + const handleFetchMore = ({ + direction, + }: { + direction: EnumCursorDirection; + }) => { + if (!validateFetchMore({ direction, pageInfo })) return; + return fetchMore({ + variables: { + cursor: + direction === EnumCursorDirection.FORWARD + ? pageInfo?.endCursor + : pageInfo?.startCursor, + limit: CLASSES_PER_PAGE, + direction, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + courseClasses: mergeCursorData({ + direction, + fetchMoreResult: fetchMoreResult.courseClasses, + prevResult: prev.courseClasses, + }), + }); + }, + }); + }; + + return { + loading, + classes, + handleFetchMore, + pageInfo, + error, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/classes/states/classDetailStates.tsx b/frontend/plugins/education_ui/src/modules/classes/states/classDetailStates.tsx new file mode 100644 index 0000000000..94ffe16fc5 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/states/classDetailStates.tsx @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +export const renderingClassDetailAtom = atom(false); +export const classDetailActiveActionTabAtom = atom(''); diff --git a/frontend/plugins/education_ui/src/modules/classes/types/ClassHotKeyScope.ts b/frontend/plugins/education_ui/src/modules/classes/types/ClassHotKeyScope.ts new file mode 100644 index 0000000000..5cabbe3765 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/types/ClassHotKeyScope.ts @@ -0,0 +1,5 @@ +export enum ClassHotKeyScope { + ClassAddSheet = 'class-add-sheet', + ClassEditSheet = 'class-edit-sheet', + ClassAddSheetDescriptionField = 'class-add-sheet-description-field', +} diff --git a/frontend/plugins/education_ui/src/modules/classes/types/type.ts b/frontend/plugins/education_ui/src/modules/classes/types/type.ts new file mode 100644 index 0000000000..35cb561715 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/classes/types/type.ts @@ -0,0 +1,7 @@ +export interface IClass { + _id: string; + name: string; + description?: string; + location: string; + type: string; +} diff --git a/frontend/plugins/education_ui/src/modules/courses/Main.tsx b/frontend/plugins/education_ui/src/modules/courses/Main.tsx new file mode 100644 index 0000000000..c4bc14d6db --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/Main.tsx @@ -0,0 +1,41 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router'; + +const CoursePage = lazy(() => + import('~/pages/CourseIndexPage').then((module) => ({ + default: module.default, + })), +); + +const CourseCategoryPage = lazy(() => + import('~/pages/CourseCategoryPage').then((module) => ({ + default: module.default, + })), +); + +const StudentPage = lazy(() => + import('~/pages/StudentIndexPage').then((module) => ({ + default: module.default, + })), +); + +const CourseAddPage = lazy(() => + import('~/pages/AddCoursePage').then((module) => ({ + default: module.default, + })), +); + +const CourseMain = () => { + return ( + }> + + } /> + } /> + } /> + } /> + + + ); +}; + +export default CourseMain; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-category/AddCourseCategoryForm.tsx b/frontend/plugins/education_ui/src/modules/courses/add-category/AddCourseCategoryForm.tsx new file mode 100644 index 0000000000..640b1805b3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-category/AddCourseCategoryForm.tsx @@ -0,0 +1,59 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button, Form, useToast, Dialog } from 'erxes-ui'; +import { CourseCategoryAddCoreFields } from '@/courses/add-category/components/CourseCategoryAddCoreFields'; +import { + courseCategoryFormSchema, + CourseCategoryFormType, +} from '@/courses/add-category/components/formSchema'; +import { useAddCourseCategory } from '@/courses/hooks/useAddCourseCategory'; +import { ApolloError } from '@apollo/client'; + +export function AddCourseCategoryForm({ + onOpenChange, +}: { + onOpenChange: (open: boolean) => void; +}) { + const { courseCategoryAdd } = useAddCourseCategory(); + const form = useForm({ + resolver: zodResolver(courseCategoryFormSchema), + }); + const { toast } = useToast(); + const onSubmit = (data: CourseCategoryFormType) => { + courseCategoryAdd({ + variables: data, + onError: (e: ApolloError) => { + console.log(e.message); + toast({ + title: 'Error', + description: e.message, + }); + }, + onCompleted: () => { + form.reset(); + onOpenChange(false); + }, + }); + }; + + return ( +
+ + + + + + + + + + + ); +} diff --git a/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddCoreFields.tsx b/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddCoreFields.tsx new file mode 100644 index 0000000000..10e864641a --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddCoreFields.tsx @@ -0,0 +1,96 @@ +import { UseFormReturn } from 'react-hook-form'; +import { Form, Input, Select } from 'erxes-ui'; +import { CourseCategoryFormType } from '@/courses/add-category/components/formSchema'; +import { useCourseCategories } from '@/courses/hooks/useCourseCategories'; + +export const CourseCategoryAddCoreFields = ({ + form, +}: { + form: UseFormReturn; +}) => { + const { courseCategories = [] } = useCourseCategories(); + + return ( + <> + ( + + NAME + + + + + + + )} + /> + ( + + Code + + + + + + + )} + /> + ( + + Description + + + + + + + )} + /> + + ( + + Parent + + + + )} + /> + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddDialog.tsx b/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddDialog.tsx new file mode 100644 index 0000000000..15c11f957c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-category/components/CourseCategoryAddDialog.tsx @@ -0,0 +1,53 @@ +import { IconPlus } from '@tabler/icons-react'; + +import { + Button, + Dialog, + Kbd, + usePreviousHotkeyScope, + useScopedHotkeys, + useSetHotkeyScope, +} from 'erxes-ui'; +import { useState } from 'react'; +import { AddCourseCategoryForm } from '@/courses/add-category/AddCourseCategoryForm'; + +export const CourseCategoryAddDialog = () => { + const setHotkeyScope = useSetHotkeyScope(); + const [open, setOpen] = useState(false); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const onOpen = () => { + setOpen(true); + setHotkeyScopeAndMemorizePreviousScope('course-category-add-sheet'); + }; + + const onClose = () => { + setOpen(false); + setHotkeyScope('course-category-page'); + }; + + useScopedHotkeys(`c`, () => onOpen(), 'course-category-page'); + useScopedHotkeys(`esc`, () => onClose(), 'course-category-add-sheet'); + + return ( + + + + + { + e.preventDefault(); + }} + > + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-category/components/formSchema.ts b/frontend/plugins/education_ui/src/modules/courses/add-category/components/formSchema.ts new file mode 100644 index 0000000000..434a5dfb32 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-category/components/formSchema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const courseCategoryFormSchema = z.object({ + name: z + .string() + .min(2, 'Name must be at least 2 characters') + .max(100, 'Name must be less than 100 characters'), + code: z.string(), + description: z.string(), + parentId: z.string().optional(), +}); + +export type CourseCategoryFormType = z.infer; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/AddCourseForm.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/AddCourseForm.tsx new file mode 100644 index 0000000000..a0b5522046 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/AddCourseForm.tsx @@ -0,0 +1,190 @@ +import { + Button, + Form, + Resizable, + Preview, + Separator, + useToast, +} from 'erxes-ui'; +import { useForm } from 'react-hook-form'; +import { IconX } from '@tabler/icons-react'; +import { + courseFormSchema, + CourseFormType, +} from '@/courses/add-course/components/formSchema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router'; +import { + CourseAddCoreFields, + CourseAddScheduleFields, + CourseAddUtilsFields, +} from '@/courses/add-course/components/steps'; +import { useAddCourse } from '@/courses/hooks/useAddCourse'; +import { ApolloError } from '@apollo/client'; + +type CourseStep = { + label: string; + component: React.ComponentType; +}; + +const courseSteps: CourseStep[] = [ + { + label: 'Үндсэн мэдээлэл оруулах', + component: CourseAddCoreFields, + }, + { label: 'Анги, хуваарь', component: CourseAddScheduleFields }, + { label: 'Төлбөр, байршил', component: CourseAddUtilsFields }, +]; + +const AddCourseForm = () => { + const [currentStep, setStep] = useState(0); + const { courseAdd } = useAddCourse(); + const { toast } = useToast(); + const navigate = useNavigate(); + + const form = useForm({ + resolver: zodResolver(courseFormSchema), + }); + + const onSubmit = (data: CourseFormType) => { + courseAdd({ + variables: data, + onError: (e: ApolloError) => { + console.log(e.message); + toast({ + title: 'Error', + description: e.message, + }); + }, + onCompleted: () => { + form.reset(); + navigate(-1); + }, + }); + }; + + const handleNextStep = () => { + if (currentStep < courseSteps.length - 1) { + return setStep((prev) => prev + 1); + } + form.handleSubmit(onSubmit)(); + }; + const handlePreviousStep = () => { + if (currentStep > 0) { + setStep((prev) => prev - 1); + } + }; + + console.log(form.formState.errors, 'Errors'); + + return ( + <> +
+

Хөтөлбөр үүсгэх

+
+ + + +
+
+ + + +
+
+
+ + АЛХАМ {currentStep + 1} + +
+

+ {courseSteps[currentStep]?.label} +

+
+
+ {courseSteps.map((_, index) => ( +
+ ))} +
+
+ +
+ + {courseSteps.map((step, index) => ( +
+ {step.component && ( +
+ +
+ )} +
+ ))} + +
+ + + +
+ {currentStep !== 0 && ( + + )} + +
+
+ + + + + + + +
+

Урьдчилан харах

+ + + + +
+
+ + + ); +}; + +export default AddCourseForm; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/CustomerAddSheet.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/CustomerAddSheet.tsx new file mode 100644 index 0000000000..4026a273f6 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/CustomerAddSheet.tsx @@ -0,0 +1,61 @@ +import { IconPlus } from '@tabler/icons-react'; + +import { + Button, + Kbd, + Sheet, + usePreviousHotkeyScope, + useScopedHotkeys, + useSetHotkeyScope, +} from 'erxes-ui'; +import { CourseHotKeyScope } from '@/courses/types/CourseHotKeyScope'; +import { useState } from 'react'; +import { AddCourseForm } from '../AddCourseForm'; + +export const CourseAddSheet = () => { + const setHotkeyScope = useSetHotkeyScope(); + const [open, setOpen] = useState(false); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const onOpen = () => { + setOpen(true); + setHotkeyScopeAndMemorizePreviousScope(CourseHotKeyScope.CourseAddSheet); + }; + + const onClose = () => { + setOpen(false); + setHotkeyScope('course-page'); + }; + + useScopedHotkeys(`c`, () => onOpen(), 'course-page'); + useScopedHotkeys(`esc`, () => onClose(), CourseHotKeyScope.CourseAddSheet); + + return ( + (open ? onOpen() : onClose())}> + + + + { + e.preventDefault(); + }} + > + + + + ); +}; + +export const CourseAddSheetHeader = () => { + return ( + + Add course + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/CategoryField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/CategoryField.tsx new file mode 100644 index 0000000000..0701c3a134 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/CategoryField.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { + Combobox, + Command, + Popover, + Skeleton, + TextOverflowTooltip, + cn, +} from 'erxes-ui'; +import { useCourseCategories } from '~/modules/courses/hooks/useCourseCategories'; + +interface Category { + _id: string; + name: string; + attachment?: { + url: string; + }; +} + +interface CategoryFieldProps { + value: string; + onChange: (value: string) => void; + className?: string; +} + +export const CategoryField: React.FC = ({ + value, + onChange, + className, +}) => { + const [open, setOpen] = useState(false); + const { courseCategories, loading } = useCourseCategories({}); + const currentValue = courseCategories?.find( + (category: Category) => category._id === value, + ); + + const handleSelectCategory = (categoryId: string) => { + onChange(categoryId === value ? '' : categoryId); + setOpen(false); + }; + + if (loading) return ; + + return ( + + + {currentValue ? ( +
+ {currentValue?.name} +
+ ) : ( + + )} +
+ + + + + + {courseCategories?.map((category: Category) => ( + handleSelectCategory(category._id)} + title={category.name} + > + + + + ))} + + + +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/ClassIdField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/ClassIdField.tsx new file mode 100644 index 0000000000..062a0a43a6 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/ClassIdField.tsx @@ -0,0 +1,47 @@ +import { Form, Select } from 'erxes-ui'; +import { Control } from 'react-hook-form'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; +import { useClasses } from '@/classes/hooks/useClasses'; + +export const ClassIdField = ({ + control, +}: { + control: Control; +}) => { + const { classes = [] } = useClasses(); + + return ( + ( + + Class + + + + )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DateField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DateField.tsx new file mode 100644 index 0000000000..500bfbf378 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DateField.tsx @@ -0,0 +1,32 @@ +import { DatePicker, Form } from 'erxes-ui'; +import { Control } from 'react-hook-form'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; + +export const DateField = ({ + control, + name, + label = 'Date', +}: { + control: Control; + name: 'startDate' | 'endDate' | 'deadline'; + label: string; +}) => { + return ( + ( + + {label} + + + + + )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DescriptionField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DescriptionField.tsx new file mode 100644 index 0000000000..b000eb86dd --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/DescriptionField.tsx @@ -0,0 +1,33 @@ +import { Control } from 'react-hook-form'; + +import { Form, Editor } from 'erxes-ui'; + +import { CourseFormType } from '@/courses/add-course/components/formSchema'; +import { CourseHotKeyScope } from '@/courses/types/CourseHotKeyScope'; + +export const DescriptionField = ({ + control, +}: { + control: Control; +}) => { + return ( + ( + + DESCRIPTION + + + + + + + )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/OwnerIdField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/OwnerIdField.tsx new file mode 100644 index 0000000000..7b0e40604a --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/OwnerIdField.tsx @@ -0,0 +1,32 @@ +import { Form } from 'erxes-ui'; +import { Control } from 'react-hook-form'; +import { AssignMember } from 'ui-modules'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; + +export const OwnerIdField = ({ + control, +}: { + control: Control; +}) => { + return ( + ( + + CHOOSE AN TEACHER + +
+ +
+
+ +
+ )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/SelectDaysField.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/SelectDaysField.tsx new file mode 100644 index 0000000000..81e45bec7d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/SelectDaysField.tsx @@ -0,0 +1,72 @@ +import { Form } from 'erxes-ui'; +import { Control } from 'react-hook-form'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; + +export const daysAsString = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', +]; + +export const SelectDaysField = ({ + control, +}: { + control: Control; +}) => { + return ( + ( + + Хичээллэх өдрүүд + +
+ {daysAsString.map((day) => { + const value: (typeof daysAsString)[number][] = Array.isArray( + field.value, + ) + ? field.value + : field.value + ? [field.value] + : []; + const isSelected = value.includes(day); + return ( + + ); + })} +
+
+
+ )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/index.ts b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/index.ts new file mode 100644 index 0000000000..b91dc407ab --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/fields/index.ts @@ -0,0 +1,6 @@ +export * from './DescriptionField'; +export * from './OwnerIdField'; +export * from './DateField'; +export * from './CategoryField'; +export * from './ClassIdField'; +export * from './SelectDaysField'; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/formSchema.ts b/frontend/plugins/education_ui/src/modules/courses/add-course/components/formSchema.ts new file mode 100644 index 0000000000..3609ffa8c5 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/formSchema.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; + +const DayEnum = z.enum([ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', +]); + +export const courseFormSchema = z.object({ + attachment: z.any().optional(), + name: z + .string() + .min(2, 'Name must be at least 2 characters') + .max(100, 'Name must be less than 100 characters'), + type: z.enum(['Training', 'Event', 'Volunteering', 'Mentorship']), + description: z.string().optional(), + unitPrice: z.number({ + required_error: 'Unit price is required', + }), + ownerId: z.string().default(''), + startDate: z.date(), + endDate: z.date(), + deadline: z.date(), + categoryId: z.string({ + required_error: 'Please select a category', + }), + classId: z.string({ + required_error: 'Please select a class', + }), + dayOfWeek: z + .array(DayEnum, { required_error: 'Please select at least one day' }) + .nonempty('Please select at least one day'), + limit: z.number(), + location: z.string(), +}); + +export type CourseFormType = z.infer; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddCoreFields.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddCoreFields.tsx new file mode 100644 index 0000000000..4cf52bf066 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddCoreFields.tsx @@ -0,0 +1,85 @@ +import { UseFormReturn } from 'react-hook-form'; +import { Form, Input, Select } from 'erxes-ui'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; +import { + CategoryField, + DescriptionField, +} from '@/courses/add-course/components/fields'; +import { COURSE_TYPE_OPTIONS } from '@/courses/constants/CourseConstants'; + +export const CourseAddCoreFields = ({ + form, +}: { + form: UseFormReturn; +}) => { + return ( + <> + ( + + NAME + + + + + + + )} + /> +
+ ( + + CATEGORY + + + + + + )} + /> + ( + + TYPE + + + + )} + /> +
+ + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddScheduleFields.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddScheduleFields.tsx new file mode 100644 index 0000000000..66141b1f22 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddScheduleFields.tsx @@ -0,0 +1,48 @@ +import { UseFormReturn } from 'react-hook-form'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; +import { + ClassIdField, + DateField, + OwnerIdField, + SelectDaysField, +} from '@/courses/add-course/components/fields'; +import { Form, Input } from 'erxes-ui'; + +export const CourseAddScheduleFields = ({ + form, +}: { + form: UseFormReturn; +}) => { + return ( +
+
+ + + + +
+ +
+ ( + + Багтаамж + + + + + + + )} + /> + +
+
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddUtilsFields.tsx b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddUtilsFields.tsx new file mode 100644 index 0000000000..d0b29b85a6 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/CourseAddUtilsFields.tsx @@ -0,0 +1,71 @@ +import { UseFormReturn } from 'react-hook-form'; +import { CourseFormType } from '@/courses/add-course/components/formSchema'; +import { CurrencyField, Form, Input, Upload } from 'erxes-ui'; +import { IconUpload } from '@tabler/icons-react'; + +export const CourseAddUtilsFields = ({ + form, +}: { + form: UseFormReturn; +}) => { + return ( +
+
+ ( + + Location + + + + + + + )} + /> + ( + + UNIT PRICE + + field.onChange(value)} + /> + + + + )} + /> +
+ ( + + UPLOAD + + + + + + Primary upload + + + + + + )} + /> +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/index.ts b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/index.ts new file mode 100644 index 0000000000..351d18b662 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/add-course/components/steps/index.ts @@ -0,0 +1,3 @@ +export * from '@/courses/add-course/components/steps/CourseAddCoreFields'; +export * from '@/courses/add-course/components/steps/CourseAddScheduleFields'; +export * from '@/courses/add-course/components/steps/CourseAddUtilsFields'; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/CourseColumns.tsx b/frontend/plugins/education_ui/src/modules/courses/components/CourseColumns.tsx new file mode 100644 index 0000000000..97e1d6b3b3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/CourseColumns.tsx @@ -0,0 +1,88 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { RecordTableInlineHead } from 'erxes-ui/modules/record-table/components/RecordTableInlineHead'; +import { ICourse } from '@/courses/types/courseType'; +import { + RecordTable, + RecordTableCellDisplay, + Slider, + TextOverflowTooltip, +} from 'erxes-ui'; +import { ActionField, SwitchField } from '@/courses/edit-course'; +import { courseMoreColumn } from './CourseMoreColumn'; + +export const courseColumns: ColumnDef[] = [ + courseMoreColumn as ColumnDef, + RecordTable.checkboxColumn as ColumnDef, + { + id: 'name', + accessorKey: 'name', + header: () => , + cell: ({ cell }) => ( + + + + ), + }, + { + id: 'class', + accessorKey: 'class', + header: () => , + cell: ({ cell }) => { + const value = cell.getValue() as { name?: string } | undefined; + const name = value?.name || ''; + return ( + + + + ); + }, + size: 100, + }, + { + id: 'description', + accessorKey: 'description', + header: () => , + cell: ({ cell }) => ( + + + + ), + }, + + { + id: 'status', + accessorKey: 'status', + size: 100, + header: () => , + cell: ({ cell }) => , + }, + { + id: 'enrollment', + accessorKey: 'enrollment', + header: () => , + cell: ({ cell }) => { + return ( +
+ + {80}% +
+ ); + }, + }, + { + id: 'teacher', + accessorKey: 'teacher', + header: () => , + cell: ({ cell }) => ( + + + + ), + }, + { + id: 'actions', + accessorKey: 'actions', + header: () => , + cell: ({ cell }) => , + }, +]; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/CourseCommandBar.tsx b/frontend/plugins/education_ui/src/modules/courses/components/CourseCommandBar.tsx new file mode 100644 index 0000000000..39eddef9aa --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/CourseCommandBar.tsx @@ -0,0 +1,32 @@ +import { IconCopy } from '@tabler/icons-react'; + +import { Button, CommandBar, RecordTable, Separator } from 'erxes-ui'; +import { CoursesDelete } from '@/courses/components/course-command-bar/CoursesDelete'; + +export const CourseCommandBar = () => { + const { table } = RecordTable.useRecordTable(); + + return ( + 0}> + + + {table.getFilteredSelectedRowModel().rows.length} selected + + + row.original._id)} + /> + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/CourseHeader.tsx b/frontend/plugins/education_ui/src/modules/courses/components/CourseHeader.tsx new file mode 100644 index 0000000000..4b59363e88 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/CourseHeader.tsx @@ -0,0 +1,35 @@ +import { IconBook, IconPlus } from '@tabler/icons-react'; +import { Breadcrumb, Button, Kbd, Separator } from 'erxes-ui'; +import { Link } from 'react-router'; +import { PageHeader } from 'ui-modules'; + +export const CourseHeader = () => { + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/CourseMoreColumn.tsx b/frontend/plugins/education_ui/src/modules/courses/components/CourseMoreColumn.tsx new file mode 100644 index 0000000000..f49faff91a --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/CourseMoreColumn.tsx @@ -0,0 +1,37 @@ +import { Cell } from '@tanstack/react-table'; +import { RecordTable, usePreviousHotkeyScope } from 'erxes-ui'; +import { useSetAtom } from 'jotai'; +import { useQueryState } from 'erxes-ui'; +import { renderingCourseDetailAtom } from '~/modules/courses/states/courseDetailStates'; +import { CourseHotKeyScope } from '@/courses/types/CourseHotKeyScope'; + +export const CourseMoreColumnCell = ({ + cell, +}: { + cell: Cell; +}) => { + const [, setOpen] = useQueryState('courseId'); + const setRenderingCourseDetail = useSetAtom(renderingCourseDetailAtom); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + const { _id } = cell.row.original; + return ( + { + setOpen(_id); + setTimeout(() => { + setHotkeyScopeAndMemorizePreviousScope( + CourseHotKeyScope.CourseEditSheet, + ); + }, 100); + setRenderingCourseDetail(false); + }} + /> + ); +}; + +export const courseMoreColumn = { + id: 'more', + cell: CourseMoreColumnCell, + size: 33, +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/CourseRecordTable.tsx b/frontend/plugins/education_ui/src/modules/courses/components/CourseRecordTable.tsx new file mode 100644 index 0000000000..c6cf913573 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/CourseRecordTable.tsx @@ -0,0 +1,41 @@ +import { RecordTable } from 'erxes-ui'; +import { useCourses } from '@/courses/hooks/useCourses'; +import { courseColumns } from '@/courses/components/CourseColumns'; +import { CourseCommandBar } from '@/courses/components/CourseCommandBar'; + +export const CourseRecordTable = () => { + const { courses, handleFetchMore, loading, pageInfo } = useCourses({}); + + const { hasPreviousPage, hasNextPage } = pageInfo || {}; + + return ( + + + + + + + {loading && } + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryHeader.tsx b/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryHeader.tsx new file mode 100644 index 0000000000..58e149f147 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryHeader.tsx @@ -0,0 +1,30 @@ +import { IconBook } from '@tabler/icons-react'; +import { Breadcrumb, Button, Separator } from 'erxes-ui'; +import { Link } from 'react-router'; +import { PageHeader } from 'ui-modules'; +import { CourseCategoryAddDialog } from '~/modules/courses/add-category/components/CourseCategoryAddDialog'; + +export const CourseCategoryHeader = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryRecordTable.tsx b/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryRecordTable.tsx new file mode 100644 index 0000000000..857c9b3cb0 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/category/CourseCategoryRecordTable.tsx @@ -0,0 +1,141 @@ +import { + RecordTable, + RecordTableCellContent, + RecordTableCellDisplay, + RecordTableCellTrigger, + RecordTablePopover, + RecordTableTree, +} from 'erxes-ui/modules/record-table'; +import { useCourseCategories } from '@/courses/hooks/useCourseCategories'; +import { ICourseCategory } from '@/courses/types/courseType'; +import { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/table-core'; +import { IconHash, IconLabelFilled, IconPackage } from '@tabler/icons-react'; +import { Input } from 'erxes-ui/components'; + +export const CourseCategoryRecordTable = () => { + const { courseCategories, loading } = useCourseCategories({}); + + const categories = courseCategories?.map((category: ICourseCategory) => ({ + ...category, + hasChildren: courseCategories?.some( + (c: ICourseCategory) => c.parentId === category._id, + ), + })); + + const categoryObject = useMemo(() => { + return categories?.reduce( + (acc: Record, category: ICourseCategory) => { + acc[category._id] = category; + return acc; + }, + {}, + ); + }, [categories]); + + return ( + + + + + + + ( + + )} + /> + {loading && } + + + + + + ); +}; + +export const courseCategoryColumns: ( + categoryObject: Record, +) => ColumnDef[] = ( + categoryObject, +) => [ + RecordTable.checkboxColumn as ColumnDef< + ICourseCategory & { hasChildren: boolean } + >, + { + id: 'name', + header: () => ( + + ), + accessorKey: 'name', + cell: ({ cell }) => { + return ( + + + + {cell.getValue() as string} + + + + + + + ); + }, + size: 300, + }, + { + id: 'code', + header: () => , + accessorKey: 'code', + cell: ({ cell }) => { + return ( + + + {cell.getValue() as string} + + + + + + ); + }, + }, + + { + id: 'courseCount', + header: () => ( + + ), + accessorKey: 'courseCount', + cell: ({ cell }) => { + return ( + + {cell.getValue() as number} + + ); + }, + }, + { + id: 'parentId', + header: () => , + accessorKey: 'parentId', + cell: ({ cell }) => { + const parent = categoryObject[cell.getValue() as string]; + return {parent?.name}; + }, + size: 300, + }, +]; diff --git a/frontend/plugins/education_ui/src/modules/courses/components/course-command-bar/CoursesDelete.tsx b/frontend/plugins/education_ui/src/modules/courses/components/course-command-bar/CoursesDelete.tsx new file mode 100644 index 0000000000..8233bac322 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/components/course-command-bar/CoursesDelete.tsx @@ -0,0 +1,36 @@ +import { Button } from 'erxes-ui/components'; +import { IconTrash } from '@tabler/icons-react'; +import { useConfirm } from 'erxes-ui/hooks'; +import { useToast } from 'erxes-ui'; +import { ApolloError } from '@apollo/client'; +import { useRemoveCourses } from '@/courses/hooks/useRemoveCourses'; + +export const CoursesDelete = ({ courseIds }: { courseIds: string[] }) => { + const { confirm } = useConfirm(); + const { removeCourses } = useRemoveCourses(); + const { toast } = useToast(); + return ( + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/constants/CourseConstants.tsx b/frontend/plugins/education_ui/src/modules/courses/constants/CourseConstants.tsx new file mode 100644 index 0000000000..3277168b6d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/constants/CourseConstants.tsx @@ -0,0 +1,6 @@ +export const COURSE_TYPE_OPTIONS = [ + { label: 'Сургалт', value: 'Training' }, + { label: 'Арга хэмжээ', value: 'Event' }, + { label: 'Сайн дурын ажил', value: 'Volunteering' }, + { label: 'Зөвлөх хөтөлбөр', value: 'Mentorship' }, +]; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseAttendances.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseAttendances.tsx new file mode 100644 index 0000000000..8128579bb2 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseAttendances.tsx @@ -0,0 +1,63 @@ +import { Skeleton, Separator } from 'erxes-ui'; +import { useCourseDetail } from '@/courses/detail/hooks/useCourseDetail'; +import { useMemo, Fragment } from 'react'; +import dayjs from 'dayjs'; + +const DEMO_STUDENTS = [ + { id: '1', name: 'Student 1' }, + { id: '2', name: 'Student 2' }, + { id: '3', name: 'Student 3' }, + { id: '4', name: 'Student 4' }, + { id: '5', name: 'Student 5' }, +]; + +export const CourseAttendances = () => { + const { courseDetail, loading } = useCourseDetail(); + const { startDate, endDate } = courseDetail || {}; + + // Helper to get all dates between startDate and endDate + const dateList = useMemo(() => { + if (!startDate || !endDate) return []; + const start = new Date(startDate); + const end = new Date(endDate); + const dates = []; + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + dates.push(new Date(d)); + } + return dates; + }, [startDate, endDate]); + + // Generate random attendance data for demo + const getRandomAttendance = () => Math.random() > 0.3; + + if (loading) { + return ; + } + + return ( +
+ +
+
Student
+ {dateList.map((date) => ( +
+ {dayjs(date).format('DD/MM/YYYY')} +
+ ))} +
+ {DEMO_STUDENTS.map((student) => ( + +
+
{student.name}
+ {dateList.map((date) => ( +
+ {getRandomAttendance() ? 'Present' : 'Absent'} +
+ ))} +
+ +
+ ))} +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseComments.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseComments.tsx new file mode 100644 index 0000000000..f9debaaa9f --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseComments.tsx @@ -0,0 +1,29 @@ +import { Skeleton, useQueryState } from 'erxes-ui'; +import { useCourseComments } from '@/courses/hooks/useCourseComments'; + +export const CourseComments = () => { + const [courseId] = useQueryState('courseId'); + const { courseComments, loading } = useCourseComments(courseId); + + if (loading) { + return ; + } + + return ( +
+ {courseComments?.map((comment) => ( +
+ {/*
+ created by +
*/} +

{comment.content}

+
+ + {new Date(comment.createdAt).toLocaleDateString()} + +
+
+ ))} +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetail.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetail.tsx new file mode 100644 index 0000000000..153f21f40c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetail.tsx @@ -0,0 +1,6 @@ +import { CourseDetailLayout } from '@/courses/detail/components/CourseDetailLayout'; +import { CourseDetailActions } from '@/courses/detail/components/CourseDetailActions'; + +export const CourseDetail = () => { + return } />; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailActions.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailActions.tsx new file mode 100644 index 0000000000..ea3ec62137 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailActions.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { + Icon, + IconActivity, + IconCalendarClock, + IconFileDescription, + IconMessage, + IconX, +} from '@tabler/icons-react'; +import { Button, Resizable, SideMenu, cn } from 'erxes-ui'; +import { useAtom, useSetAtom } from 'jotai'; +import { courseDetailActiveActionTabAtom } from '@/courses/detail/states/courseDetailStates'; +import { CourseDetailGeneral } from '@/courses/detail/components/CourseDetailGeneral'; +import { CourseAttendances } from '@/courses/detail/components/CourseAttendances'; +import { CourseComments } from '@/courses/detail/components/CourseComments'; + +const actionTabs = { + detail: { + title: 'Detail', + icon: IconFileDescription, + code: 'detail', + }, + activity: { + title: 'Activity', + icon: IconActivity, + code: 'activity', + }, + comments: { + title: 'Comments', + icon: IconMessage, + code: 'comments', + }, + attendances: { + title: 'Attendances', + icon: IconCalendarClock, + code: 'attendances', + }, +}; + +export const CourseDetailActions = () => { + const [activeTab, setActiveTab] = useAtom(courseDetailActiveActionTabAtom); + + return ( + <> + + setActiveTab(value)} + className={cn('h-full')} + > + + + + +
Activity
+
+ + + + + + +
+
+ + + ); +}; + +export const CustomerDetailActionsTrigger = () => { + const [activeTab, setActiveTab] = useAtom(courseDetailActiveActionTabAtom); + + return ( +
+ setActiveTab(value)} + className="h-full" + > + + {Object.values(actionTabs).map((tab) => ( + + ))} + + +
+ ); +}; + +export const ActionTabsContent = ({ + children, + value, + title, + icon, +}: { + children: React.ReactNode; + value: string; + title?: string; + icon: Icon; +}) => { + // const [activeTab] = useAtom(customerDetailActiveActionTabAtom); + + return ( + + {/* */} + {children} + + ); +}; + +export const ActionHeader = (props: { title?: string; icon: Icon }) => { + const setActiveTab = useSetAtom(courseDetailActiveActionTabAtom); + return ( +
+ +

{props.title}

+ +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailGeneral.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailGeneral.tsx new file mode 100644 index 0000000000..710d220e65 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailGeneral.tsx @@ -0,0 +1,75 @@ +import { Input, Label, Skeleton, Slider, Switch } from 'erxes-ui'; +import { useCourseDetail } from '@/courses/detail/hooks/useCourseDetail'; + +export const CourseDetailGeneral = () => { + const { courseDetail, loading } = useCourseDetail(); + const { status, class: courseClass, location } = courseDetail || {}; + + if (loading) { + return ; + } + + return ( +
+
+
+ + console.log('classId', e.target.value)} + className="w-full rounded-md border-gray-200 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 transition-all" + /> +
+
+ + console.log('location', e.target.value)} + className="w-full rounded-md border-gray-200 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 transition-all" + /> +
+
+
+ + { + console.log('checked'); + }} + /> +
+
+
+ +
+ + {80}% +
+
+
+
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailLayout.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailLayout.tsx new file mode 100644 index 0000000000..3c8a15c041 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailLayout.tsx @@ -0,0 +1,23 @@ +import { Resizable } from 'erxes-ui'; +import { CourseDetailSheet } from './CourseDetailSheet'; + +export const CourseDetailLayout = ({ + actions, +}: { + actions?: React.ReactNode; +}) => { + return ( + +
+
+ + {actions} + +
+
+
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailSheet.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailSheet.tsx new file mode 100644 index 0000000000..555eaabdda --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/components/CourseDetailSheet.tsx @@ -0,0 +1,55 @@ +import { IconLayoutSidebarLeftCollapse } from '@tabler/icons-react'; +import { + Button, + cn, + Sheet, + usePreviousHotkeyScope, + useQueryState, +} from 'erxes-ui'; +import { useCourseDetail } from '@/courses/detail/hooks/useCourseDetail'; +import { useAtomValue } from 'jotai'; +import { courseDetailActiveActionTabAtom } from '@/courses/states/courseDetailStates'; + +export const CourseDetailSheet = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [open, setOpen] = useQueryState('courseId'); + const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope(); + + const activeTab = useAtomValue(courseDetailActiveActionTabAtom); + + const { courseDetail } = useCourseDetail(); + + const { name } = courseDetail || {}; + + return ( + { + setOpen(null); + goBackToPreviousHotkeyScope(); + }} + > + + + + {name} + + + Course Detail + + + {children} + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/graphql/queries/courseDetail.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/graphql/queries/courseDetail.tsx new file mode 100644 index 0000000000..72b223c132 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/graphql/queries/courseDetail.tsx @@ -0,0 +1,47 @@ +import { gql } from '@apollo/client'; + +export const COURSE_DETAIL = gql` + query CourseDetail($_id: String!) { + courseDetail(_id: $_id) { + _id + name + categoryId + category { + _id + name + description + parentId + code + order + isRoot + } + description + createdAt + type + attachment { + url + name + type + size + duration + } + status + startDate + endDate + deadline + unitPrice + commentCount + location + class { + _id + name + description + location + level + createdAt + updatedAt + } + classId + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/hooks/useCourseDetail.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/hooks/useCourseDetail.tsx new file mode 100644 index 0000000000..02a1af9a76 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/hooks/useCourseDetail.tsx @@ -0,0 +1,27 @@ +import { OperationVariables, useQuery } from '@apollo/client'; +import { useSetAtom } from 'jotai'; +import { useQueryState } from 'erxes-ui'; +import { renderingCourseDetailAtom } from '@/courses/detail/states/courseDetailStates'; +import { COURSE_DETAIL } from '@/courses/detail/graphql/queries/courseDetail'; + +export const useCourseDetail = (operationVariables?: OperationVariables) => { + const [_id] = useQueryState('courseId'); + const setRendering = useSetAtom(renderingCourseDetailAtom); + const { data, loading } = useQuery(COURSE_DETAIL, { + variables: { + _id, + }, + skip: !_id, + ...operationVariables, + onCompleted: (data) => { + setRendering(false); + operationVariables?.onCompleted?.(data); + }, + onError: (error) => { + setRendering(false); + operationVariables?.onError?.(error); + }, + }); + + return { courseDetail: data?.courseDetail, loading }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/detail/states/courseDetailStates.tsx b/frontend/plugins/education_ui/src/modules/courses/detail/states/courseDetailStates.tsx new file mode 100644 index 0000000000..79cac8ae6c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/detail/states/courseDetailStates.tsx @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +export const renderingCourseDetailAtom = atom(false); +export const courseDetailActiveActionTabAtom = atom('detail'); diff --git a/frontend/plugins/education_ui/src/modules/courses/edit-course/ActionField.tsx b/frontend/plugins/education_ui/src/modules/courses/edit-course/ActionField.tsx new file mode 100644 index 0000000000..0ebf835a4f --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/edit-course/ActionField.tsx @@ -0,0 +1,14 @@ +import { CurrencyDisplay, CurrencyField, useToast } from 'erxes-ui'; +import { CoreCell } from '@tanstack/react-table'; +import { useCourseEdit } from '@/courses/hooks/useCourseEdit'; +import { ApolloError } from '@apollo/client'; +import { + RecordTableInlineCell, + RecordTableInlineCellEditForm, +} from 'erxes-ui/modules/record-table/record-table-cell/components/RecordTableInlineCell'; +import { useState } from 'react'; + +export const ActionField = ({ cell }: { cell: CoreCell }) => { + const { toast } = useToast(); + return
; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/edit-course/PriceField.tsx b/frontend/plugins/education_ui/src/modules/courses/edit-course/PriceField.tsx new file mode 100644 index 0000000000..0767000aaa --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/edit-course/PriceField.tsx @@ -0,0 +1,56 @@ +import { CurrencyCode, CurrencyField, useToast } from 'erxes-ui'; +import { CoreCell } from '@tanstack/react-table'; +import { useCourseEdit } from '@/courses/hooks/useCourseEdit'; +import { ApolloError } from '@apollo/client'; +import { + RecordTableInlineCell, + RecordTableInlineCellEditForm, +} from 'erxes-ui/modules/record-table/record-table-cell/components/RecordTableInlineCell'; +import { useState } from 'react'; + +export const PriceField = ({ cell }: { cell: CoreCell }) => { + const [value, setValue] = useState(cell.getValue() as number); + + const { courseEdit } = useCourseEdit(); + const { toast } = useToast(); + return ( + { + courseEdit({ + variables: { + id: cell.row.original._id, + [cell.column.id]: value, + }, + onError: (e: ApolloError) => { + toast({ + title: 'Error', + description: e.message, + }); + }, + }); + }} + getValue={() => cell.getValue()} + value={value} + display={() => ( + setValue(value)} + /> + // + )} + edit={() => ( + + setValue(value)} + /> + + )} + /> + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/edit-course/SwitchField.tsx b/frontend/plugins/education_ui/src/modules/courses/edit-course/SwitchField.tsx new file mode 100644 index 0000000000..e0f0f00dc0 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/edit-course/SwitchField.tsx @@ -0,0 +1,31 @@ +import { Switch, useToast } from 'erxes-ui'; +import { CoreCell } from '@tanstack/react-table'; +import { useChangeCourseStatus } from '@/courses/hooks/useChangeCourseStatus'; +import { ApolloError } from '@apollo/client'; + +export const SwitchField = ({ cell }: { cell: CoreCell }) => { + const { changeCourseStatus } = useChangeCourseStatus(); + const { toast } = useToast(); + return ( +
+ { + changeCourseStatus({ + variables: { + id: cell.row.original._id, + status: checked ? 'active' : 'draft', + }, + onError: (e: ApolloError) => { + toast({ + title: 'Error', + description: e.message, + }); + }, + }); + }} + /> +
+ ); +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/edit-course/index.ts b/frontend/plugins/education_ui/src/modules/courses/edit-course/index.ts new file mode 100644 index 0000000000..f5c94439f3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/edit-course/index.ts @@ -0,0 +1,3 @@ +export * from './SwitchField'; +export * from './PriceField'; +export * from './ActionField'; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourse.ts b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourse.ts new file mode 100644 index 0000000000..d6fd1c5ed7 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourse.ts @@ -0,0 +1,55 @@ +import { gql } from '@apollo/client'; + +export const ADD_COURSES = gql` + mutation courseAdd( + $name: String! + $categoryId: String! + $classId: String! + $startDate: Date! + $unitPrice: Float! + $type: String! + $description: String + $attachment: AttachmentInput + $endDate: Date + $deadline: Date + $status: String + $limit: Int + ) { + courseAdd( + name: $name + categoryId: $categoryId + classId: $classId + startDate: $startDate + unitPrice: $unitPrice + type: $type + description: $description + attachment: $attachment + endDate: $endDate + deadline: $deadline + status: $status + limit: $limit + ) { + _id + attachment { + duration + name + size + type + url + } + categoryId + commentCount + createdAt + deadline + description + endDate + name + classId + startDate + status + type + limit + unitPrice + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourseCategory.ts b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourseCategory.ts new file mode 100644 index 0000000000..179e7deaf7 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/addCourseCategory.ts @@ -0,0 +1,33 @@ +import { gql } from '@apollo/client'; + +export const ADD_COURSE_CATEGORY = gql` + mutation courseCategoryAdd( + $name: String! + $code: String! + $description: String + $parentId: String + ) { + courseCategoryAdd( + name: $name + code: $code + description: $description + parentId: $parentId + ) { + _id + name + description + parentId + code + order + isRoot + courseCount + attachment { + url + name + type + size + duration + } + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/changeCourseStatus.ts b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/changeCourseStatus.ts new file mode 100644 index 0000000000..d51948a2c3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/changeCourseStatus.ts @@ -0,0 +1,20 @@ +import { gql } from '@apollo/client'; + +export const CHANGE_COURSE_STATUS = gql` + mutation ChangeCourseStatus($id: String!, $status: StatusType) { + changeCourseStatus(_id: $id, status: $status) { + _id + name + categoryId + description + createdAt + type + status + startDate + endDate + deadline + unitPrice + commentCount + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/editCourse.ts b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/editCourse.ts new file mode 100644 index 0000000000..ca5838c2df --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/editCourse.ts @@ -0,0 +1,43 @@ +import { gql } from '@apollo/client'; + +export const EDIT_COURSE = gql` + mutation CourseEdit( + $id: String! + $name: String! + $categoryId: String! + $startDate: Date! + $unitPrice: Float! + $type: String! + $status: String + ) { + courseEdit( + _id: $id + name: $name + categoryId: $categoryId + startDate: $startDate + unitPrice: $unitPrice + type: $type + status: $status + ) { + _id + attachment { + duration + name + size + type + url + } + categoryId + commentCount + createdAt + deadline + description + endDate + name + startDate + status + type + unitPrice + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/removeCourses.ts b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/removeCourses.ts new file mode 100644 index 0000000000..a9c0590b0d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/mutations/removeCourses.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const REMOVE_COURSES = gql` + mutation CourseRemove($courseIds: [String]) { + courseRemove(courseIds: $courseIds) + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getComments.tsx b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getComments.tsx new file mode 100644 index 0000000000..19922c1c9f --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getComments.tsx @@ -0,0 +1,38 @@ +import { gql } from '@apollo/client'; + +export const GET_COMMENTS = gql` + query CourseComments( + $courseId: String! + $parentId: String + $page: Int + $perPage: Int + $direction: CURSOR_DIRECTION + $cursor: String + ) { + courseComments( + courseId: $courseId + parentId: $parentId + page: $page + perPage: $perPage + direction: $direction + cursor: $cursor + ) { + totalCount + list { + _id + courseId + parentId + content + childCount + createdAt + updatedAt + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourse.tsx b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourse.tsx new file mode 100644 index 0000000000..fd14ec0d94 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourse.tsx @@ -0,0 +1,63 @@ +import { gql } from '@apollo/client'; + +export const GET_COURSES = gql` + query Courses( + $page: Int + $perPage: Int + $categoryId: String + $ids: [String] + $searchValue: String + $sortField: String + $sortDirection: Int + $statuses: [String] + $direction: CURSOR_DIRECTION + $cursor: String + ) { + courses( + page: $page + perPage: $perPage + categoryId: $categoryId + ids: $ids + searchValue: $searchValue + sortField: $sortField + sortDirection: $sortDirection + statuses: $statuses + direction: $direction + cursor: $cursor + ) { + totalCount + list { + _id + attachment { + duration + name + size + type + url + } + categoryId + commentCount + createdAt + deadline + description + endDate + name + startDate + status + type + unitPrice + classId + class { + _id + name + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourseCategory.tsx b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourseCategory.tsx new file mode 100644 index 0000000000..8e2fea2ced --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/graphql/queries/getCourseCategory.tsx @@ -0,0 +1,23 @@ +import { gql } from '@apollo/client'; + +export const GET_COURSE_CATEGORY = gql` + query CourseCategories($parentId: String, $searchValue: String) { + courseCategories(parentId: $parentId, searchValue: $searchValue) { + _id + name + description + parentId + code + order + isRoot + courseCount + attachment { + url + name + type + size + duration + } + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourse.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourse.tsx new file mode 100644 index 0000000000..7f2eda0920 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourse.tsx @@ -0,0 +1,57 @@ +import { useMutation, ApolloCache, MutationHookOptions } from '@apollo/client'; +import { ICourse } from '@/courses/types/courseType'; +import { ADD_COURSES } from '@/courses/graphql/mutations/addCourse'; +import { GET_COURSES } from '@/courses/graphql/queries/getCourse'; +import { useRecordTableCursor } from 'erxes-ui/modules'; + +interface CourseData { + courses: { + list: ICourse[]; + totalCount: number; + }; +} + +interface AddCourseResult { + courseAdd: ICourse; +} + +export function useAddCourse( + options?: MutationHookOptions, +) { + const { cursor } = useRecordTableCursor({ + sessionKey: 'course_cursor', + }); + const [courseAdd, { loading, error }] = useMutation( + ADD_COURSES, + { + ...options, + update: (cache: ApolloCache, { data }) => { + try { + const queryVariables = { perPage: 30, cursor }; + const existingData = cache.readQuery({ + query: GET_COURSES, + variables: queryVariables, + }); + if (!existingData || !existingData.courses || !data?.courseAdd) + return; + + cache.writeQuery({ + query: GET_COURSES, + variables: queryVariables, + data: { + courses: { + ...existingData.courses, + list: [data.courseAdd, ...existingData.courses.list], + totalCount: existingData.courses.totalCount + 1, + }, + }, + }); + } catch (e) { + console.error('error:', e); + } + }, + }, + ); + + return { courseAdd, loading, error }; +} diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourseCategory.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourseCategory.tsx new file mode 100644 index 0000000000..0f150a6904 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useAddCourseCategory.tsx @@ -0,0 +1,43 @@ +import { useMutation, ApolloCache, MutationHookOptions } from '@apollo/client'; +import { ICourseCategory } from '@/courses/types/courseType'; +import { ADD_COURSE_CATEGORY } from '@/courses/graphql/mutations/addCourseCategory'; +import { GET_COURSE_CATEGORY } from '@/courses/graphql/queries/getCourseCategory'; + +interface CourseCategoryData { + courseCategories: ICourseCategory[]; +} + +interface AddCourseCategoryResult { + courseCategoryAdd: ICourseCategory; +} + +export function useAddCourseCategory( + options?: MutationHookOptions, +) { + const [courseCategoryAdd, { loading, error }] = + useMutation(ADD_COURSE_CATEGORY, { + ...options, + update: (cache: ApolloCache, { data }) => { + try { + const existingData = cache.readQuery({ + query: GET_COURSE_CATEGORY, + }); + if (!existingData || !data?.courseCategoryAdd) return; + + cache.writeQuery({ + query: GET_COURSE_CATEGORY, + data: { + courseCategories: [ + data.courseCategoryAdd, + ...existingData.courseCategories, + ], + }, + }); + } catch (e) { + console.error('error:', e); + } + }, + }); + + return { courseCategoryAdd, loading, error }; +} diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useChangeCourseStatus.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useChangeCourseStatus.tsx new file mode 100644 index 0000000000..e013edb6b9 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useChangeCourseStatus.tsx @@ -0,0 +1,25 @@ +import { MutationHookOptions, useMutation } from '@apollo/client'; +import { CHANGE_COURSE_STATUS } from '@/courses/graphql/mutations/changeCourseStatus'; + +export const useChangeCourseStatus = () => { + const [changeCourseStatus, { loading }] = useMutation(CHANGE_COURSE_STATUS); + + const mutate = ({ variables, ...options }: MutationHookOptions) => { + changeCourseStatus({ + ...options, + variables, + update: (cache, { data: { changeCourseStatus } }) => { + cache.modify({ + id: cache.identify(changeCourseStatus), + fields: Object.keys(variables || {}).reduce((fields: any, field) => { + fields[field] = () => (variables || {})[field]; + return fields; + }, {}), + optimistic: true, + }); + }, + }); + }; + + return { changeCourseStatus: mutate, loading }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseCategories.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseCategories.tsx new file mode 100644 index 0000000000..e12df15763 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseCategories.tsx @@ -0,0 +1,15 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; + +import { ICourseCategory } from '@/courses/types/courseType'; +import { GET_COURSE_CATEGORY } from '@/courses/graphql/queries/getCourseCategory'; + +export const useCourseCategories = (options?: QueryHookOptions) => { + const { data, loading } = useQuery<{ + courseCategories: ICourseCategory[]; + }>(GET_COURSE_CATEGORY, options); + + return { + courseCategories: data?.courseCategories, + loading, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseComments.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseComments.tsx new file mode 100644 index 0000000000..a31e3d907d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseComments.tsx @@ -0,0 +1,69 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { + useRecordTableCursor, + IRecordTableCursorPageInfo, + mergeCursorData, + EnumCursorDirection, + validateFetchMore, +} from 'erxes-ui'; +import { ICourseComment } from '@/courses/types/commentsType'; +import { GET_COMMENTS } from '@/courses/graphql/queries/getComments'; + +export const useCourseComments = ( + courseId: string | null, + options?: QueryHookOptions, +) => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'course_comments_cursor', + }); + + const { data, loading, fetchMore } = useQuery<{ + courseComments: { + list: ICourseComment[]; + pageInfo: IRecordTableCursorPageInfo; + }; + }>(GET_COMMENTS, { + ...options, + variables: { + cursor, + courseId, + }, + skip: !courseId, + }); + + const { list: courseComments, pageInfo } = data?.courseComments || {}; + + const handleFetchMore = ({ + direction, + }: { + direction: EnumCursorDirection; + }) => { + if (!validateFetchMore({ direction, pageInfo })) return; + return fetchMore({ + variables: { + cursor: + direction === EnumCursorDirection.FORWARD + ? pageInfo?.endCursor + : pageInfo?.startCursor, + direction, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + courseComments: mergeCursorData({ + direction, + fetchMoreResult: fetchMoreResult.courseComments, + prevResult: prev.courseComments, + }), + }); + }, + }); + }; + + return { + loading, + courseComments, + handleFetchMore, + pageInfo, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseEdit.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseEdit.tsx new file mode 100644 index 0000000000..9338e9466d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourseEdit.tsx @@ -0,0 +1,25 @@ +import { MutationHookOptions, useMutation } from '@apollo/client'; +import { EDIT_COURSE } from '@/courses/graphql/mutations/editCourse'; + +export const useCourseEdit = () => { + const [courseEdit, { loading }] = useMutation(EDIT_COURSE); + + const mutate = ({ variables, ...options }: MutationHookOptions) => { + courseEdit({ + ...options, + variables, + update: (cache, { data: { courseEdit } }) => { + cache.modify({ + id: cache.identify(courseEdit), + fields: Object.keys(variables || {}).reduce((fields: any, field) => { + fields[field] = () => (variables || {})[field]; + return fields; + }, {}), + optimistic: true, + }); + }, + }); + }; + + return { courseEdit: mutate, loading }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useCourses.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourses.tsx new file mode 100644 index 0000000000..c2cb51ac65 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useCourses.tsx @@ -0,0 +1,68 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { GET_COURSES } from '@/courses/graphql/queries/getCourse'; +import { ICourse } from '@/courses/types/courseType'; +import { + useRecordTableCursor, + IRecordTableCursorPageInfo, + mergeCursorData, + EnumCursorDirection, + validateFetchMore, +} from 'erxes-ui'; + +export const COURSES_PER_PAGE = 30; + +export const useCourses = (options?: QueryHookOptions) => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'course_cursor', + }); + + const { data, loading, fetchMore } = useQuery<{ + courses: { + list: ICourse[]; + pageInfo: IRecordTableCursorPageInfo; + }; + }>(GET_COURSES, { + ...options, + variables: { + perPage: COURSES_PER_PAGE, + cursor, + }, + }); + + const { list: courses, pageInfo } = data?.courses || {}; + + const handleFetchMore = ({ + direction, + }: { + direction: EnumCursorDirection; + }) => { + if (!validateFetchMore({ direction, pageInfo })) return; + return fetchMore({ + variables: { + cursor: + direction === EnumCursorDirection.FORWARD + ? pageInfo?.endCursor + : pageInfo?.startCursor, + limit: COURSES_PER_PAGE, + direction, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + courses: mergeCursorData({ + direction, + fetchMoreResult: fetchMoreResult.courses, + prevResult: prev.courses, + }), + }); + }, + }); + }; + + return { + loading, + courses, + handleFetchMore, + pageInfo, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/hooks/useRemoveCourses.tsx b/frontend/plugins/education_ui/src/modules/courses/hooks/useRemoveCourses.tsx new file mode 100644 index 0000000000..312225ee93 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/hooks/useRemoveCourses.tsx @@ -0,0 +1,45 @@ +import { OperationVariables, useMutation } from '@apollo/client'; +import { REMOVE_COURSES } from '@/courses/graphql/mutations/removeCourses'; +import { GET_COURSES } from '@/courses/graphql/queries/getCourse'; +import { ICourse } from '@/courses/types/courseType'; +import { useRecordTableCursor } from 'erxes-ui/modules'; + +export const useRemoveCourses = () => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'course_cursor', + }); + const [_removeCourses, { loading }] = useMutation(REMOVE_COURSES); + + const removeCourses = async ( + courseIds: string[], + options?: OperationVariables, + ) => { + await _removeCourses({ + ...options, + variables: { courseIds, ...options?.variables }, + update: (cache) => { + try { + cache.updateQuery( + { + query: GET_COURSES, + variables: { perPage: 30, cursor }, + }, + ({ courses }) => ({ + courses: { + ...courses, + list: courses.list.filter( + (courses: ICourse) => !courseIds.includes(courses._id), + ), + totalCount: courses.totalCount - courseIds.length, + }, + }), + ); + } catch (e) { + console.log(e); + } + }, + }); + }; + + return { removeCourses, loading }; +}; diff --git a/frontend/plugins/education_ui/src/modules/courses/states/courseDetailStates.tsx b/frontend/plugins/education_ui/src/modules/courses/states/courseDetailStates.tsx new file mode 100644 index 0000000000..75f53bb69e --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/states/courseDetailStates.tsx @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +export const renderingCourseDetailAtom = atom(false); +export const courseDetailActiveActionTabAtom = atom(''); diff --git a/frontend/plugins/education_ui/src/modules/courses/types/CourseHotKeyScope.ts b/frontend/plugins/education_ui/src/modules/courses/types/CourseHotKeyScope.ts new file mode 100644 index 0000000000..b16b2a2712 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/types/CourseHotKeyScope.ts @@ -0,0 +1,5 @@ +export enum CourseHotKeyScope { + CourseAddSheet = 'course-add-sheet', + CourseEditSheet = 'course-edit-sheet', + CourseAddSheetDescriptionField = 'course-add-sheet-description-field', +} diff --git a/frontend/plugins/education_ui/src/modules/courses/types/commentsType.ts b/frontend/plugins/education_ui/src/modules/courses/types/commentsType.ts new file mode 100644 index 0000000000..6cdf7be5aa --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/types/commentsType.ts @@ -0,0 +1,9 @@ +export interface ICourseComment { + _id: string; + type: string; + content: string; + parentId?: string; + courseId: string; + updatedAt: Date; + createdAt: Date; +} diff --git a/frontend/plugins/education_ui/src/modules/courses/types/courseType.ts b/frontend/plugins/education_ui/src/modules/courses/types/courseType.ts new file mode 100644 index 0000000000..cc856d7310 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/courses/types/courseType.ts @@ -0,0 +1,29 @@ +import { IAttachment } from 'erxes-ui/types'; + +export interface ICourse { + _id: string; + name: string; + categoryId: string; + description?: string; + createdAt?: Date; + type?: string; + attachment?: any; + status?: string; + startDate: Date; + endDate?: Date; + deadline?: Date; + unitPrice: number; + limit: number; +} + +export interface ICourseCategory { + _id: string; + name: string; + description?: string; + parentId?: string; + code: string; + isRoot?: boolean; + activityCount?: number; + order: string; + attachment?: IAttachment; +} diff --git a/frontend/plugins/education_ui/src/modules/students/Main.tsx b/frontend/plugins/education_ui/src/modules/students/Main.tsx new file mode 100644 index 0000000000..98a9a78353 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/Main.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router'; + +const Student = lazy(() => + import('~/pages/StudentIndexPage').then((module) => ({ + default: module.default, + })), +); + +const StudentMain = () => { + return ( + }> + + } /> + + + ); +}; + +export default StudentMain; diff --git a/frontend/plugins/education_ui/src/modules/students/components/StudentColumns.tsx b/frontend/plugins/education_ui/src/modules/students/components/StudentColumns.tsx new file mode 100644 index 0000000000..090084fbe3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/components/StudentColumns.tsx @@ -0,0 +1,108 @@ +import type { ColumnDef } from '@tanstack/react-table'; +import { RecordTableInlineHead } from 'erxes-ui/modules/record-table/components/RecordTableInlineHead'; +import { + Avatar, + InlineCell, + RecordTable, + RecordTableCellDisplay, + Switch, + TextOverflowTooltip, +} from 'erxes-ui'; +import { IStudent } from '@/students/types/type'; +import { + IconAlignLeft, + IconChecks, + IconMail, + IconUser, +} from '@tabler/icons-react'; + +const checkBoxColumn = RecordTable.checkboxColumn as ColumnDef; + +export const studentColumns: ColumnDef[] = [ + checkBoxColumn, + { + id: 'avatar', + accessorKey: 'avatar', + header: () => , + cell: ({ cell }) => ( + ( + + + + {cell.row.original.details.firstName?.charAt(0) || + cell.row.original.details.lastName?.charAt(0) || + cell.row.original.email?.charAt(0)} + + + )} + /> + ), + size: 34, + }, + { + id: 'firstName', + accessorKey: 'firstName', + header: () => ( + + ), + cell: ({ cell }) => { + const { + details: { firstName }, + } = cell.row.original; + return ( + + + + ); + }, + }, + { + id: 'lastName', + accessorKey: 'lastName', + header: () => ( + + ), + cell: ({ cell }) => { + const { + details: { lastName }, + } = cell.row.original; + return ( + + + + ); + }, + }, + { + id: 'email', + accessorKey: 'email', + header: () => , + cell: ({ cell }) => { + const { email } = cell.row.original; + return ( + + + + ); + }, + }, + { + id: 'isActive', + accessorKey: 'isActive', + header: () => , + cell: ({ cell }) => ( + ( + + )} + /> + ), + }, +]; diff --git a/frontend/plugins/education_ui/src/modules/students/components/StudentCommandBar.tsx b/frontend/plugins/education_ui/src/modules/students/components/StudentCommandBar.tsx new file mode 100644 index 0000000000..1781f91b9a --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/components/StudentCommandBar.tsx @@ -0,0 +1,27 @@ +import { IconCopy, IconTrash } from '@tabler/icons-react'; + +import { Button, CommandBar, RecordTable, Separator } from 'erxes-ui'; + +export const StudentCommandBar = () => { + const { table } = RecordTable.useRecordTable(); + + return ( + 0}> + + + {table.getFilteredSelectedRowModel().rows.length} selected + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/students/components/StudentHeader.tsx b/frontend/plugins/education_ui/src/modules/students/components/StudentHeader.tsx new file mode 100644 index 0000000000..a18b1f11f1 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/components/StudentHeader.tsx @@ -0,0 +1,26 @@ +import { IconSchool } from '@tabler/icons-react'; +import { Breadcrumb, Button, Separator } from 'erxes-ui'; +import { Link } from 'react-router'; +import { PageHeader } from 'ui-modules'; + +export const StudentHeader = () => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/students/components/StudentRecordTable.tsx b/frontend/plugins/education_ui/src/modules/students/components/StudentRecordTable.tsx new file mode 100644 index 0000000000..0ccaa9543d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/components/StudentRecordTable.tsx @@ -0,0 +1,41 @@ +import { RecordTable } from 'erxes-ui'; +import { studentColumns } from '@/students/components/StudentColumns'; +import { StudentCommandBar } from '@/students/components/StudentCommandBar'; +import { useStudents } from '@/students/hooks/useStudents'; + +export const StudentRecordTable = () => { + const { students, handleFetchMore, loading, pageInfo } = useStudents({}); + + const { hasPreviousPage, hasNextPage } = pageInfo || {}; + + return ( + + + + + + + {loading && } + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/students/graphql/queries/getStudents.tsx b/frontend/plugins/education_ui/src/modules/students/graphql/queries/getStudents.tsx new file mode 100644 index 0000000000..d24512a282 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/graphql/queries/getStudents.tsx @@ -0,0 +1,49 @@ +import { gql } from '@apollo/client'; + +export const GET_STUDENTS = gql` + query Students( + $page: Int + $perPage: Int + $direction: CURSOR_DIRECTION + $cursor: String + ) { + students( + page: $page + perPage: $perPage + direction: $direction + cursor: $cursor + ) { + list { + _id + username + email + details { + avatar + coverPhoto + fullName + shortName + birthDate + position + workStartedDate + location + description + operatorPhone + firstName + middleName + lastName + employeeId + } + links + isActive + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/students/hooks/useStudents.tsx b/frontend/plugins/education_ui/src/modules/students/hooks/useStudents.tsx new file mode 100644 index 0000000000..5d0b5ec4a7 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/hooks/useStudents.tsx @@ -0,0 +1,69 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { IStudent } from '@/students/types/type'; +import { + useRecordTableCursor, + IRecordTableCursorPageInfo, + mergeCursorData, + EnumCursorDirection, + validateFetchMore, +} from 'erxes-ui'; +import { GET_STUDENTS } from '@/students/graphql/queries/getStudents'; + +export const STUDENTS_PER_PAGE = 30; + +export const useStudents = (options?: QueryHookOptions) => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'student_cursor', + }); + + const { data, loading, error, fetchMore } = useQuery<{ + students: { + list: IStudent[]; + pageInfo: IRecordTableCursorPageInfo; + }; + }>(GET_STUDENTS, { + ...options, + variables: { + perPage: STUDENTS_PER_PAGE, + cursor, + }, + }); + + const { list: students, pageInfo } = data?.students || {}; + + const handleFetchMore = ({ + direction, + }: { + direction: EnumCursorDirection; + }) => { + if (!validateFetchMore({ direction, pageInfo })) return; + return fetchMore({ + variables: { + cursor: + direction === EnumCursorDirection.FORWARD + ? pageInfo?.endCursor + : pageInfo?.startCursor, + limit: STUDENTS_PER_PAGE, + direction, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + students: mergeCursorData({ + direction, + fetchMoreResult: fetchMoreResult.students, + prevResult: prev.students, + }), + }); + }, + }); + }; + + return { + loading, + students, + handleFetchMore, + pageInfo, + error, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/students/states/classDetailStates.tsx b/frontend/plugins/education_ui/src/modules/students/states/classDetailStates.tsx new file mode 100644 index 0000000000..7aa0473eb5 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/states/classDetailStates.tsx @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +export const renderingStudentDetailAtom = atom(false); +export const studentDetailActiveActionTabAtom = atom(''); diff --git a/frontend/plugins/education_ui/src/modules/students/types/type.ts b/frontend/plugins/education_ui/src/modules/students/types/type.ts new file mode 100644 index 0000000000..2f97fb3235 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/students/types/type.ts @@ -0,0 +1,22 @@ +export interface ILink { + [key: string]: string; +} + +export interface IUsersDetails { + avatar: string; + fullName: string; + firstName: string; + lastName: string; + operatorPhone: string; +} + +export interface IStudent { + _id: string; + username?: string; + password: string; + email?: string; + details: IUsersDetails; + links?: ILink; + isActive?: boolean; + deviceTokens?: string[]; +} diff --git a/frontend/plugins/education_ui/src/modules/teachers/Main.tsx b/frontend/plugins/education_ui/src/modules/teachers/Main.tsx new file mode 100644 index 0000000000..60ace4d533 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/Main.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router'; + +const TeacherPage = lazy(() => + import('~/pages/TeacherIndexPage').then((module) => ({ + default: module.default, + })), +); + +const TeachersMain = () => { + return ( + }> + + } /> + + + ); +}; + +export default TeachersMain; diff --git a/frontend/plugins/education_ui/src/modules/teachers/add-teacher/AddTeacherForm.tsx b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/AddTeacherForm.tsx new file mode 100644 index 0000000000..3a744a4a81 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/AddTeacherForm.tsx @@ -0,0 +1,92 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button, Form, Dialog, MultipleSelector, Skeleton } from 'erxes-ui'; +import { + teacherFormSchema, + TeacherFormType, +} from '@/teachers/add-teacher/components/formSchema'; +import { useUsers } from '@/teachers/hooks/useUsers'; +import { IUser } from '@/teachers/types/teacherType'; + +type Option = { + label: string; + value: string; +}; + +export function AddTeacherForm({ + onOpenChange, +}: { + onOpenChange: (open: boolean) => void; +}) { + const { users, loading } = useUsers({}); + + console.log(users); + + // const { classAdd } = useAddClass(); + const form = useForm({ + resolver: zodResolver(teacherFormSchema), + }); + const onSubmit = (data: TeacherFormType) => { + // classAdd({ + // variables: data, + // onError: (e: ApolloError) => { + // console.log(e.message); + // toast({ + // title: 'Error', + // description: e.message, + // }); + // }, + // onCompleted: () => { + // form.reset(); + // onOpenChange(false); + // }, + // }); + }; + + if (loading) return ; + + const modifiedArray: Option[] = users?.map(({ email, _id }: IUser) => ({ + label: email, + value: _id, + })); + + return ( +
+ + + ( +
+ Select users + No results found

+ } + /> +
+ )} + /> + +
+ + + + + + +
+ + ); +} diff --git a/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/TeacherAddDialog.tsx b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/TeacherAddDialog.tsx new file mode 100644 index 0000000000..29f9232311 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/TeacherAddDialog.tsx @@ -0,0 +1,54 @@ +import { IconPlus } from '@tabler/icons-react'; + +import { + Button, + Dialog, + Kbd, + usePreviousHotkeyScope, + useScopedHotkeys, + useSetHotkeyScope, +} from 'erxes-ui'; +import { useState } from 'react'; +import { TeacherHotKeyScope } from '@/teachers/types/TeacherHotKeyScope'; +import { AddTeacherForm } from '@/teachers/add-teacher/AddTeacherForm'; + +export const TeacherAddDialog = () => { + const setHotkeyScope = useSetHotkeyScope(); + const [open, setOpen] = useState(false); + const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); + + const onOpen = () => { + setOpen(true); + setHotkeyScopeAndMemorizePreviousScope(TeacherHotKeyScope.TeacherAddSheet); + }; + + const onClose = () => { + setOpen(false); + setHotkeyScope('teacher-page'); + }; + + useScopedHotkeys(`c`, () => onOpen(), 'teacher-page'); + useScopedHotkeys(`esc`, () => onClose(), TeacherHotKeyScope.TeacherAddSheet); + + return ( + + + + + { + e.preventDefault(); + }} + > + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/formSchema.ts b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/formSchema.ts new file mode 100644 index 0000000000..c00e65649c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/add-teacher/components/formSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const teacherFormSchema = z.object({ + userIds: z.array(z.string()), +}); + +export type TeacherFormType = z.infer; diff --git a/frontend/plugins/education_ui/src/modules/teachers/components/TeacherColumns.tsx b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherColumns.tsx new file mode 100644 index 0000000000..c7d3124030 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherColumns.tsx @@ -0,0 +1,117 @@ +import { + IconAlignLeft, + IconMail, + IconNumber, + IconPhone, +} from '@tabler/icons-react'; +import type { ColumnDef } from '@tanstack/react-table'; + +import { + RecordTable, + TextOverflowTooltip, + RecordTableCellDisplay, +} from 'erxes-ui'; +import { ITeacher } from '@/teachers/types/teacherType'; + +const checkBoxColumn = RecordTable.checkboxColumn as ColumnDef; + +export const teacherColumns: ColumnDef[] = [ + checkBoxColumn, + { + id: 'firstName', + accessorKey: 'user', + header: () => ( + + ), + cell: ({ cell }) => { + const value = cell.getValue() as ITeacher['user']; + const { firstName } = value?.details || {}; + return ( + + + + ); + }, + }, + { + id: 'lastName', + accessorKey: 'user', + header: () => ( + + ), + cell: ({ cell }) => { + const value = cell.getValue() as ITeacher['user']; + const { lastName } = value?.details || {}; + return ( + + + + ); + }, + }, + { + id: 'employeeId', + accessorKey: 'user', + header: () => ( + + ), + cell: ({ cell }) => { + const value = cell.getValue() as ITeacher['user']; + const employeeId = value?.employeeId || ''; + return ( + + + + ); + }, + }, + + { + id: 'operatorPhone', + accessorKey: 'user', + header: () => ( + + ), + cell: ({ cell }) => { + const value = cell.getValue() as ITeacher['user']; + const { operatorPhone } = value?.details || {}; + return ( + + + + ); + }, + }, + + { + id: 'email', + accessorKey: 'user', + header: () => , + cell: ({ cell }) => { + const value = cell.getValue() as ITeacher['user']; + const email = value?.email || ''; + return ( + + + + ); + }, + }, + + // { + // id: 'email', + // accessorKey: 'email', + // header: () => , + // cell: ({ cell }) => { + // const { email, _id } = cell.row.original; + // return ( + // + // {email} + // + // + // + // + // ); + // }, + // }, +]; diff --git a/frontend/plugins/education_ui/src/modules/teachers/components/TeacherHeader.tsx b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherHeader.tsx new file mode 100644 index 0000000000..d0799d9ed3 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherHeader.tsx @@ -0,0 +1,30 @@ +import { IconSchool } from '@tabler/icons-react'; +import { Breadcrumb, Button, Separator } from 'erxes-ui'; +import { Link } from 'react-router'; +import { PageHeader } from 'ui-modules'; +import { TeacherAddDialog } from '@/teachers/add-teacher/components/TeacherAddDialog'; + +export const TeacherHeader = () => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/plugins/education_ui/src/modules/teachers/components/TeacherTable.tsx b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherTable.tsx new file mode 100644 index 0000000000..0aa41774cc --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/components/TeacherTable.tsx @@ -0,0 +1,50 @@ +import { RecordTable } from 'erxes-ui'; +import { useTeachers } from '@/teachers/hooks/useTeachers'; +import { teacherColumns } from '@/teachers/components/TeacherColumns'; + +const TeacherTable = () => { + const { teachers, handleFetchMore, loading, error, pageInfo } = useTeachers( + {}, + ); + + const { hasPreviousPage, hasNextPage } = pageInfo || {}; + + if (error) { + return ( +
+ Error loading members: {error.message} +
+ ); + } + return ( + + + + + + + {loading && } + + + + + + + ); +}; + +export { TeacherTable }; diff --git a/frontend/plugins/education_ui/src/modules/teachers/graphql/mutations/addClass.ts b/frontend/plugins/education_ui/src/modules/teachers/graphql/mutations/addClass.ts new file mode 100644 index 0000000000..cac780f44c --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/graphql/mutations/addClass.ts @@ -0,0 +1,24 @@ +import { gql } from '@apollo/client'; + +export const ADD_CLASS = gql` + mutation classAdd( + $level: String! + $name: String + $description: String + $location: String + ) { + classAdd( + level: $level + name: $name + description: $description + location: $location + ) { + _id + description + location + level + createdAt + updatedAt + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getTeachers.tsx b/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getTeachers.tsx new file mode 100644 index 0000000000..cb8b91a154 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getTeachers.tsx @@ -0,0 +1,43 @@ +import { gql } from '@apollo/client'; + +export const GET_TEACHERS = gql` + query teachers( + $page: Int + $perPage: Int + $cursor: String + $direction: CURSOR_DIRECTION + ) { + teachers( + page: $page + perPage: $perPage + cursor: $cursor + direction: $direction + ) { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + list { + _id + userId + user { + _id + createdAt + username + email + isActive + details { + avatar + fullName + shortName + operatorPhone + } + employeeId + } + } + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getUsers.tsx b/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getUsers.tsx new file mode 100644 index 0000000000..f522b96e20 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/graphql/queries/getUsers.tsx @@ -0,0 +1,31 @@ +import { gql } from '@apollo/client'; +import { + GQL_CURSOR_PARAM_DEFS, + GQL_CURSOR_PARAMS, + GQL_PAGE_INFO, +} from 'erxes-ui/constants'; + +export const GET_USERS = gql` + query Users( + $searchValue: String + $ids: [String] + $excludeIds: Boolean + ${GQL_CURSOR_PARAM_DEFS} + ) { + users( + searchValue: $searchValue + ids: $ids + excludeIds: $excludeIds + ${GQL_CURSOR_PARAMS} + ) { + list { + _id + details { + avatar + fullName + } + } + ${GQL_PAGE_INFO} + } + } +`; diff --git a/frontend/plugins/education_ui/src/modules/teachers/hooks/useTeachers.tsx b/frontend/plugins/education_ui/src/modules/teachers/hooks/useTeachers.tsx new file mode 100644 index 0000000000..c1e71c5012 --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/hooks/useTeachers.tsx @@ -0,0 +1,65 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { + useRecordTableCursor, + IRecordTableCursorPageInfo, + mergeCursorData, + EnumCursorDirection, + validateFetchMore, +} from 'erxes-ui'; +import { ITeacher } from '@/teachers/types/teacherType'; +import { GET_TEACHERS } from '~/modules/teachers/graphql/queries/getTeachers'; + +export const useTeachers = (options?: QueryHookOptions) => { + const { cursor } = useRecordTableCursor({ + sessionKey: 'teacher_cursor', + }); + + const { data, loading, fetchMore, error } = useQuery<{ + teachers: { + list: ITeacher[]; + pageInfo: IRecordTableCursorPageInfo; + }; + }>(GET_TEACHERS, { + ...options, + variables: { + cursor, + }, + }); + + const { list: teachers, pageInfo } = data?.teachers || {}; + + const handleFetchMore = ({ + direction, + }: { + direction: EnumCursorDirection; + }) => { + if (!validateFetchMore({ direction, pageInfo })) return; + return fetchMore({ + variables: { + cursor: + direction === EnumCursorDirection.FORWARD + ? pageInfo?.endCursor + : pageInfo?.startCursor, + direction, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + teachers: mergeCursorData({ + direction, + fetchMoreResult: fetchMoreResult.teachers, + prevResult: prev.teachers, + }), + }); + }, + }); + }; + + return { + loading, + teachers, + handleFetchMore, + pageInfo, + error, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/teachers/hooks/useUsers.tsx b/frontend/plugins/education_ui/src/modules/teachers/hooks/useUsers.tsx new file mode 100644 index 0000000000..72af473e3b --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/hooks/useUsers.tsx @@ -0,0 +1,44 @@ +import { OperationVariables, useQuery } from '@apollo/client'; +import { GET_USERS } from '@/teachers/graphql/queries/getUsers'; + +export const USERS_PER_PAGE = 30; + +export const useUsers = (options?: OperationVariables) => { + const { data, loading, error, fetchMore } = useQuery(GET_USERS, { + ...options, + variables: { + perPage: USERS_PER_PAGE, + ...options?.variables, + }, + }); + + const { + list: users, + usersTotalCount: totalCount, + pageInfo, + } = data?.users || {}; + + const handleFetchMore = () => + totalCount > users?.length && + fetchMore({ + variables: { + page: Math.ceil(users.length / USERS_PER_PAGE) + 1, + perPage: USERS_PER_PAGE, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + users: [...(prev.users || []), ...fetchMoreResult.users], + }); + }, + }); + + return { + loading, + users, + error, + totalCount, + handleFetchMore, + pageInfo, + }; +}; diff --git a/frontend/plugins/education_ui/src/modules/teachers/types/TeacherHotKeyScope.ts b/frontend/plugins/education_ui/src/modules/teachers/types/TeacherHotKeyScope.ts new file mode 100644 index 0000000000..2cad8d478d --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/types/TeacherHotKeyScope.ts @@ -0,0 +1,5 @@ +export enum TeacherHotKeyScope { + TeacherAddSheet = 'teacher-add-sheet', + TeacherEditSheet = 'teacher-edit-sheet', + TeacherAddSheetDescriptionField = 'teacher-add-sheet-description-field', +} diff --git a/frontend/plugins/education_ui/src/modules/teachers/types/teacherType.ts b/frontend/plugins/education_ui/src/modules/teachers/types/teacherType.ts new file mode 100644 index 0000000000..35ccf2e77e --- /dev/null +++ b/frontend/plugins/education_ui/src/modules/teachers/types/teacherType.ts @@ -0,0 +1,21 @@ +export interface ITeacher { + _id: string; + userId: string; + user: IUser; +} + +export interface IUser { + _id: string; + details: IUsersDetails; + email: string; + isActive: boolean; + employeeId?: string; +} + +export interface IUsersDetails { + avatar: string; + fullName: string; + firstName: string; + lastName: string; + operatorPhone: string; +} diff --git a/frontend/plugins/education_ui/src/pages/AddCoursePage.tsx b/frontend/plugins/education_ui/src/pages/AddCoursePage.tsx new file mode 100644 index 0000000000..3986d8dafd --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/AddCoursePage.tsx @@ -0,0 +1,11 @@ +import AddCourseForm from '~/modules/courses/add-course/AddCourseForm'; + +const AddCoursePage = () => { + return ( +
+ +
+ ); +}; + +export default AddCoursePage; diff --git a/frontend/plugins/education_ui/src/pages/ClassIndexPage.tsx b/frontend/plugins/education_ui/src/pages/ClassIndexPage.tsx new file mode 100644 index 0000000000..eda5255de5 --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/ClassIndexPage.tsx @@ -0,0 +1,13 @@ +import { ClassesHeader } from '~/modules/classes/components/ClassesHeader'; +import { ClassesRecordTable } from '~/modules/classes/components/ClassesRecordTable'; + +const ClassIndexPage = () => { + return ( +
+ + +
+ ); +}; + +export default ClassIndexPage; diff --git a/frontend/plugins/education_ui/src/pages/CourseCategoryPage.tsx b/frontend/plugins/education_ui/src/pages/CourseCategoryPage.tsx new file mode 100644 index 0000000000..837dafc6f6 --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/CourseCategoryPage.tsx @@ -0,0 +1,13 @@ +import { CourseCategoryHeader } from '~/modules/courses/components/category/CourseCategoryHeader'; +import { CourseCategoryRecordTable } from '~/modules/courses/components/category/CourseCategoryRecordTable'; + +const CourseCategoryPage = () => { + return ( +
+ + +
+ ); +}; + +export default CourseCategoryPage; diff --git a/frontend/plugins/education_ui/src/pages/CourseIndexPage.tsx b/frontend/plugins/education_ui/src/pages/CourseIndexPage.tsx new file mode 100644 index 0000000000..ddf14f7191 --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/CourseIndexPage.tsx @@ -0,0 +1,15 @@ +import { CourseHeader } from '~/modules/courses/components/CourseHeader'; +import { CourseRecordTable } from '~/modules/courses/components/CourseRecordTable'; +import { CourseDetail } from '~/modules/courses/detail/components/CourseDetail'; + +const CourseIndexPage = () => { + return ( +
+ + + +
+ ); +}; + +export default CourseIndexPage; diff --git a/frontend/plugins/education_ui/src/pages/StudentIndexPage.tsx b/frontend/plugins/education_ui/src/pages/StudentIndexPage.tsx new file mode 100644 index 0000000000..96714a9352 --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/StudentIndexPage.tsx @@ -0,0 +1,13 @@ +import { StudentHeader } from '~/modules/students/components/StudentHeader'; +import { StudentRecordTable } from '~/modules/students/components/StudentRecordTable'; + +const StudentIndexPage = () => { + return ( +
+ + +
+ ); +}; + +export default StudentIndexPage; diff --git a/frontend/plugins/education_ui/src/pages/TeacherIndexPage.tsx b/frontend/plugins/education_ui/src/pages/TeacherIndexPage.tsx new file mode 100644 index 0000000000..605921377d --- /dev/null +++ b/frontend/plugins/education_ui/src/pages/TeacherIndexPage.tsx @@ -0,0 +1,13 @@ +import { TeacherTable } from '@/teachers/components/TeacherTable'; +import { TeacherHeader } from '@/teachers/components/TeacherHeader'; + +const TeacherIndexPage = () => { + return ( +
+ + +
+ ); +}; + +export default TeacherIndexPage; diff --git a/frontend/plugins/education_ui/src/remote-entry.ts b/frontend/plugins/education_ui/src/remote-entry.ts new file mode 100644 index 0000000000..9b1af13490 --- /dev/null +++ b/frontend/plugins/education_ui/src/remote-entry.ts @@ -0,0 +1 @@ +export { default } from './modules/app/components/app'; diff --git a/frontend/plugins/education_ui/src/types/path/EducationsPath.tsx b/frontend/plugins/education_ui/src/types/path/EducationsPath.tsx new file mode 100644 index 0000000000..ebc911c918 --- /dev/null +++ b/frontend/plugins/education_ui/src/types/path/EducationsPath.tsx @@ -0,0 +1,5 @@ +export enum EducationsPath { + Course = 'course', + Category = 'category', + Settings = 'settings', +} diff --git a/frontend/plugins/education_ui/src/widgets/Widgets.tsx b/frontend/plugins/education_ui/src/widgets/Widgets.tsx new file mode 100644 index 0000000000..1b1a88276c --- /dev/null +++ b/frontend/plugins/education_ui/src/widgets/Widgets.tsx @@ -0,0 +1,13 @@ +const Widgets = ({ + contentType, + contentId, + message, +}: { + contentType: string; + contentId: string; + message: string; +}) => { + return
Widgets education {message}
; +}; + +export default Widgets; diff --git a/frontend/plugins/education_ui/tsconfig.app.json b/frontend/plugins/education_ui/tsconfig.app.json new file mode 100644 index 0000000000..aa683fa659 --- /dev/null +++ b/frontend/plugins/education_ui/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/frontend/plugins/education_ui/tsconfig.json b/frontend/plugins/education_ui/tsconfig.json new file mode 100644 index 0000000000..f34134997e --- /dev/null +++ b/frontend/plugins/education_ui/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "paths": { + "erxes-ui/*": [ + "frontend/libs/erxes-ui/src/*" + ], + "erxes-ui": [ + "frontend/libs/erxes-ui/src" + ], + "~/*": [ + "frontend/plugins/education_ui/src/*" + ], + "~": [ + "frontend/plugins/education_ui/src" + ], + "@/*": [ + "frontend/plugins/education_ui/src/modules/*" + ], + "@": [ + "frontend/plugins/education_ui/src/modules" + ], + "ui-modules": [ + "frontend/libs/ui-modules/src" + ] + } + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} \ No newline at end of file diff --git a/frontend/plugins/education_ui/tsconfig.spec.json b/frontend/plugins/education_ui/tsconfig.spec.json new file mode 100644 index 0000000000..9a0a6a6bea --- /dev/null +++ b/frontend/plugins/education_ui/tsconfig.spec.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "jsx": "react-jsx", + "types": [ + "jest", + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a8f4ceb2c..5893fded63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,6 +776,12 @@ importers: specifier: workspace:^ version: link:../../erxes-api-shared + backend/plugins/education_api: + dependencies: + erxes-api-shared: + specifier: workspace:^ + version: link:../../erxes-api-shared + backend/plugins/frontline_api: dependencies: aws-sdk: