make admin approve api
This commit is contained in:
parent
6acb536aef
commit
5d6cab229f
7 changed files with 616 additions and 1 deletions
100
Backend/src/controllers/AdminCourseApprovalController.ts
Normal file
100
Backend/src/controllers/AdminCourseApprovalController.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { Body, Get, Path, Post, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
|
||||||
|
import { ValidationError } from '../middleware/errorHandler';
|
||||||
|
import { AdminCourseApprovalService } from '../services/AdminCourseApproval.service';
|
||||||
|
import {
|
||||||
|
ListPendingCoursesResponse,
|
||||||
|
GetCourseDetailForAdminResponse,
|
||||||
|
ApproveCourseBody,
|
||||||
|
ApproveCourseResponse,
|
||||||
|
RejectCourseBody,
|
||||||
|
RejectCourseResponse,
|
||||||
|
} from '../types/AdminCourseApproval.types';
|
||||||
|
|
||||||
|
@Route('api/admin/courses')
|
||||||
|
@Tags('Admin/CourseApproval')
|
||||||
|
export class AdminCourseApprovalController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ดึงรายการคอร์สที่รอการอนุมัติ
|
||||||
|
* Get all courses pending for approval
|
||||||
|
*/
|
||||||
|
@Get('pending')
|
||||||
|
@Security('jwt', ['admin'])
|
||||||
|
@SuccessResponse('200', 'Pending courses retrieved successfully')
|
||||||
|
@Response('401', 'Unauthorized')
|
||||||
|
@Response('403', 'Forbidden - Admin only')
|
||||||
|
public async listPendingCourses(@Request() request: any): Promise<ListPendingCoursesResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await AdminCourseApprovalService.listPendingCourses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ดึงรายละเอียดคอร์สสำหรับการตรวจสอบ
|
||||||
|
* Get course details for admin review
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
*/
|
||||||
|
@Get('{courseId}')
|
||||||
|
@Security('jwt', ['admin'])
|
||||||
|
@SuccessResponse('200', 'Course details retrieved successfully')
|
||||||
|
@Response('401', 'Unauthorized')
|
||||||
|
@Response('403', 'Forbidden - Admin only')
|
||||||
|
@Response('404', 'Course not found')
|
||||||
|
public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await AdminCourseApprovalService.getCourseDetail(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* อนุมัติคอร์ส
|
||||||
|
* Approve a course for publication
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
*/
|
||||||
|
@Post('{courseId}/approve')
|
||||||
|
@Security('jwt', ['admin'])
|
||||||
|
@SuccessResponse('200', 'Course approved successfully')
|
||||||
|
@Response('400', 'Course is not pending for approval')
|
||||||
|
@Response('401', 'Unauthorized')
|
||||||
|
@Response('403', 'Forbidden - Admin only')
|
||||||
|
@Response('404', 'Course not found')
|
||||||
|
public async approveCourse(
|
||||||
|
@Request() request: any,
|
||||||
|
@Path() courseId: number,
|
||||||
|
@Body() body?: ApproveCourseBody
|
||||||
|
): Promise<ApproveCourseResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ปฏิเสธคอร์ส
|
||||||
|
* Reject a course (requires comment)
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
*/
|
||||||
|
@Post('{courseId}/reject')
|
||||||
|
@Security('jwt', ['admin'])
|
||||||
|
@SuccessResponse('200', 'Course rejected successfully')
|
||||||
|
@Response('400', 'Course is not pending for approval or comment is required')
|
||||||
|
@Response('401', 'Unauthorized')
|
||||||
|
@Response('403', 'Forbidden - Admin only')
|
||||||
|
@Response('404', 'Course not found')
|
||||||
|
public async rejectCourse(
|
||||||
|
@Request() request: any,
|
||||||
|
@Path() courseId: number,
|
||||||
|
@Body() body: RejectCourseBody
|
||||||
|
): Promise<RejectCourseResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,8 @@ import {
|
||||||
UpdateMyCourseResponse,
|
UpdateMyCourseResponse,
|
||||||
DeleteMyCourseResponse,
|
DeleteMyCourseResponse,
|
||||||
submitCourseResponse,
|
submitCourseResponse,
|
||||||
listinstructorCourseResponse
|
listinstructorCourseResponse,
|
||||||
|
GetCourseApprovalsResponse
|
||||||
} from '../types/CoursesInstructor.types';
|
} from '../types/CoursesInstructor.types';
|
||||||
import { CreateCourseValidator } from "../validators/CoursesInstructor.validator";
|
import { CreateCourseValidator } from "../validators/CoursesInstructor.validator";
|
||||||
|
|
||||||
|
|
@ -134,6 +135,25 @@ export class CoursesInstructorController {
|
||||||
return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId });
|
return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ดึงประวัติการส่งอนุมัติคอร์สทั้งหมด
|
||||||
|
* Get all course approval history
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
*/
|
||||||
|
@Get('{courseId}/approvals')
|
||||||
|
@Security('jwt', ['instructor'])
|
||||||
|
@SuccessResponse('200', 'Course approvals retrieved successfully')
|
||||||
|
@Response('401', 'Invalid or expired token')
|
||||||
|
@Response('403', 'You are not an instructor of this course')
|
||||||
|
@Response('404', 'Course not found')
|
||||||
|
public async getCourseApprovals(@Request() request: any, @Path() courseId: number): Promise<GetCourseApprovalsResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await CoursesInstructorService.getCourseApprovals(token, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ดึงรายชื่อผู้สอนทั้งหมดในคอร์ส
|
* ดึงรายชื่อผู้สอนทั้งหมดในคอร์ส
|
||||||
* Get list of all instructors in a specific course
|
* Get list of all instructors in a specific course
|
||||||
|
|
|
||||||
293
Backend/src/services/AdminCourseApproval.service.ts
Normal file
293
Backend/src/services/AdminCourseApproval.service.ts
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
import { prisma } from '../config/database';
|
||||||
|
import { config } from '../config';
|
||||||
|
import { logger } from '../config/logger';
|
||||||
|
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import {
|
||||||
|
ListPendingCoursesResponse,
|
||||||
|
GetCourseDetailForAdminResponse,
|
||||||
|
ApproveCourseResponse,
|
||||||
|
RejectCourseResponse,
|
||||||
|
} from '../types/AdminCourseApproval.types';
|
||||||
|
|
||||||
|
export class AdminCourseApprovalService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending courses for admin review
|
||||||
|
*/
|
||||||
|
static async listPendingCourses(): Promise<ListPendingCoursesResponse> {
|
||||||
|
try {
|
||||||
|
const courses = await prisma.course.findMany({
|
||||||
|
where: { status: 'PENDING' },
|
||||||
|
orderBy: { updated_at: 'desc' },
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
},
|
||||||
|
instructors: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chapters: {
|
||||||
|
include: {
|
||||||
|
lessons: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
courseApprovals: {
|
||||||
|
where: { action: 'SUBMITTED' },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
take: 1,
|
||||||
|
include: {
|
||||||
|
submitter: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = courses.map(course => ({
|
||||||
|
id: course.id,
|
||||||
|
title: course.title as { th: string; en: string },
|
||||||
|
slug: course.slug,
|
||||||
|
description: course.description as { th: string; en: string },
|
||||||
|
thumbnail_url: course.thumbnail_url,
|
||||||
|
status: course.status,
|
||||||
|
created_at: course.created_at,
|
||||||
|
updated_at: course.updated_at,
|
||||||
|
created_by: course.created_by,
|
||||||
|
creator: course.creator,
|
||||||
|
instructors: course.instructors.map(i => ({
|
||||||
|
user_id: i.user_id,
|
||||||
|
is_primary: i.is_primary,
|
||||||
|
user: i.user
|
||||||
|
})),
|
||||||
|
chapters_count: course.chapters.length,
|
||||||
|
lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0),
|
||||||
|
latest_submission: course.courseApprovals[0] ? {
|
||||||
|
id: course.courseApprovals[0].id,
|
||||||
|
submitted_by: course.courseApprovals[0].submitted_by,
|
||||||
|
created_at: course.courseApprovals[0].created_at,
|
||||||
|
submitter: course.courseApprovals[0].submitter
|
||||||
|
} : null
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Pending courses retrieved successfully',
|
||||||
|
data,
|
||||||
|
total: data.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to list pending courses', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get course details for admin review
|
||||||
|
*/
|
||||||
|
static async getCourseDetail(courseId: number): Promise<GetCourseDetailForAdminResponse> {
|
||||||
|
try {
|
||||||
|
const course = await prisma.course.findUnique({
|
||||||
|
where: { id: courseId },
|
||||||
|
include: {
|
||||||
|
category: {
|
||||||
|
select: { id: true, name: true }
|
||||||
|
},
|
||||||
|
creator: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
},
|
||||||
|
instructors: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chapters: {
|
||||||
|
orderBy: { sort_order: 'asc' },
|
||||||
|
include: {
|
||||||
|
lessons: {
|
||||||
|
orderBy: { sort_order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
type: true,
|
||||||
|
sort_order: true,
|
||||||
|
is_published: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
courseApprovals: {
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
include: {
|
||||||
|
submitter: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
},
|
||||||
|
reviewer: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
throw new NotFoundError('Course not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Course details retrieved successfully',
|
||||||
|
data: {
|
||||||
|
id: course.id,
|
||||||
|
title: course.title as { th: string; en: string },
|
||||||
|
slug: course.slug,
|
||||||
|
description: course.description as { th: string; en: string },
|
||||||
|
thumbnail_url: course.thumbnail_url,
|
||||||
|
price: Number(course.price),
|
||||||
|
is_free: course.is_free,
|
||||||
|
have_certificate: course.have_certificate,
|
||||||
|
status: course.status,
|
||||||
|
created_at: course.created_at,
|
||||||
|
updated_at: course.updated_at,
|
||||||
|
category: course.category ? {
|
||||||
|
id: course.category.id,
|
||||||
|
name: course.category.name as { th: string; en: string }
|
||||||
|
} : null,
|
||||||
|
creator: course.creator,
|
||||||
|
instructors: course.instructors.map(i => ({
|
||||||
|
user_id: i.user_id,
|
||||||
|
is_primary: i.is_primary,
|
||||||
|
user: i.user
|
||||||
|
})),
|
||||||
|
chapters: course.chapters.map(ch => ({
|
||||||
|
id: ch.id,
|
||||||
|
title: ch.title as { th: string; en: string },
|
||||||
|
sort_order: ch.sort_order,
|
||||||
|
is_published: ch.is_published,
|
||||||
|
lessons: ch.lessons.map(l => ({
|
||||||
|
id: l.id,
|
||||||
|
title: l.title as { th: string; en: string },
|
||||||
|
type: l.type,
|
||||||
|
sort_order: l.sort_order,
|
||||||
|
is_published: l.is_published
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
approval_history: course.courseApprovals.map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
action: a.action,
|
||||||
|
comment: a.comment,
|
||||||
|
created_at: a.created_at,
|
||||||
|
submitter: a.submitter,
|
||||||
|
reviewer: a.reviewer
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get course detail', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve a course
|
||||||
|
*/
|
||||||
|
static async approveCourse(token: string, courseId: number, comment?: string): Promise<ApproveCourseResponse> {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||||
|
|
||||||
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
||||||
|
if (!course) {
|
||||||
|
throw new NotFoundError('Course not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (course.status !== 'PENDING') {
|
||||||
|
throw new ValidationError('Course is not pending for approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
// Update course status
|
||||||
|
prisma.course.update({
|
||||||
|
where: { id: courseId },
|
||||||
|
data: { status: 'APPROVED' }
|
||||||
|
}),
|
||||||
|
// Create approval record
|
||||||
|
prisma.courseApproval.create({
|
||||||
|
data: {
|
||||||
|
course_id: courseId,
|
||||||
|
submitted_by: course.created_by,
|
||||||
|
reviewed_by: decoded.id,
|
||||||
|
action: 'APPROVED',
|
||||||
|
previous_status: course.status,
|
||||||
|
new_status: 'APPROVED',
|
||||||
|
comment: comment || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Course approved successfully'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to approve course', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a course
|
||||||
|
*/
|
||||||
|
static async rejectCourse(token: string, courseId: number, comment: string): Promise<RejectCourseResponse> {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||||
|
|
||||||
|
const course = await prisma.course.findUnique({ where: { id: courseId } });
|
||||||
|
if (!course) {
|
||||||
|
throw new NotFoundError('Course not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (course.status !== 'PENDING') {
|
||||||
|
throw new ValidationError('Course is not pending for approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comment || comment.trim() === '') {
|
||||||
|
throw new ValidationError('Comment is required when rejecting a course');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
// Update course status back to DRAFT
|
||||||
|
prisma.course.update({
|
||||||
|
where: { id: courseId },
|
||||||
|
data: { status: 'DRAFT' }
|
||||||
|
}),
|
||||||
|
// Create rejection record
|
||||||
|
prisma.courseApproval.create({
|
||||||
|
data: {
|
||||||
|
course_id: courseId,
|
||||||
|
submitted_by: course.created_by,
|
||||||
|
reviewed_by: decoded.id,
|
||||||
|
action: 'REJECTED',
|
||||||
|
previous_status: course.status,
|
||||||
|
new_status: 'DRAFT',
|
||||||
|
comment: comment
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Course rejected successfully'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to reject course', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -208,6 +208,43 @@ export class CoursesInstructorService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getCourseApprovals(token: string, courseId: number): Promise<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: any[];
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
||||||
|
|
||||||
|
// Validate instructor access
|
||||||
|
await this.validateCourseInstructor(token, courseId);
|
||||||
|
|
||||||
|
const approvals = await prisma.courseApproval.findMany({
|
||||||
|
where: { course_id: courseId },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
include: {
|
||||||
|
submitter: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
},
|
||||||
|
reviewer: {
|
||||||
|
select: { id: true, username: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Course approvals retrieved successfully',
|
||||||
|
data: approvals,
|
||||||
|
total: approvals.length,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to retrieve course approvals', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise<addinstructorCourseResponse> {
|
static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise<addinstructorCourseResponse> {
|
||||||
|
|
|
||||||
136
Backend/src/types/AdminCourseApproval.types.ts
Normal file
136
Backend/src/types/AdminCourseApproval.types.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { Course, CourseApproval, User } from '@prisma/client';
|
||||||
|
import { MultiLanguageText } from './index';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Admin Course Approval Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface PendingCourseData {
|
||||||
|
id: number;
|
||||||
|
title: MultiLanguageText;
|
||||||
|
slug: string;
|
||||||
|
description: MultiLanguageText;
|
||||||
|
thumbnail_url: string | null;
|
||||||
|
status: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date | null;
|
||||||
|
created_by: number;
|
||||||
|
creator: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
instructors: {
|
||||||
|
user_id: number;
|
||||||
|
is_primary: boolean;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
chapters_count: number;
|
||||||
|
lessons_count: number;
|
||||||
|
latest_submission: {
|
||||||
|
id: number;
|
||||||
|
submitted_by: number;
|
||||||
|
created_at: Date;
|
||||||
|
submitter: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListPendingCoursesResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: PendingCourseData[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CourseDetailForAdmin {
|
||||||
|
id: number;
|
||||||
|
title: MultiLanguageText;
|
||||||
|
slug: string;
|
||||||
|
description: MultiLanguageText;
|
||||||
|
thumbnail_url: string | null;
|
||||||
|
price: number;
|
||||||
|
is_free: boolean;
|
||||||
|
have_certificate: boolean;
|
||||||
|
status: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date | null;
|
||||||
|
category: {
|
||||||
|
id: number;
|
||||||
|
name: MultiLanguageText;
|
||||||
|
} | null;
|
||||||
|
creator: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
instructors: {
|
||||||
|
user_id: number;
|
||||||
|
is_primary: boolean;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
chapters: {
|
||||||
|
id: number;
|
||||||
|
title: MultiLanguageText;
|
||||||
|
sort_order: number;
|
||||||
|
is_published: boolean;
|
||||||
|
lessons: {
|
||||||
|
id: number;
|
||||||
|
title: MultiLanguageText;
|
||||||
|
type: string;
|
||||||
|
sort_order: number;
|
||||||
|
is_published: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
approval_history: {
|
||||||
|
id: number;
|
||||||
|
action: string;
|
||||||
|
comment: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
submitter: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
reviewer: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCourseDetailForAdminResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: CourseDetailForAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApproveCourseBody {
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApproveCourseResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RejectCourseBody {
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RejectCourseResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
@ -140,3 +140,32 @@ export interface sendCourseForReview {
|
||||||
token: string;
|
token: string;
|
||||||
course_id: number;
|
course_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CourseApprovalData {
|
||||||
|
id: number;
|
||||||
|
course_id: number;
|
||||||
|
submitted_by: number;
|
||||||
|
reviewed_by: number | null;
|
||||||
|
action: 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'REVISION_REQUESTED';
|
||||||
|
previous_status: string | null;
|
||||||
|
new_status: string | null;
|
||||||
|
comment: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
submitter: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
reviewer: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCourseApprovalsResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: CourseApprovalData[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue