feat: implement announcement management service with CRUD operations, attachment handling, and role-based access control for courses.
This commit is contained in:
parent
90d50dc84a
commit
baf389643b
3 changed files with 640 additions and 8 deletions
188
Backend/src/controllers/announcementsController.ts
Normal file
188
Backend/src/controllers/announcementsController.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { Body, Delete, Get, Path, Post, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, UploadedFile } from 'tsoa';
|
||||
import { ValidationError } from '../middleware/errorHandler';
|
||||
import { AnnouncementsService } from '../services/announcements.service';
|
||||
import {
|
||||
ListAnnouncementResponse,
|
||||
CreateAnnouncementResponse,
|
||||
UpdateAnnouncementResponse,
|
||||
DeleteAnnouncementResponse,
|
||||
UploadAnnouncementAttachmentResponse,
|
||||
DeleteAnnouncementAttachmentResponse,
|
||||
CreateAnnouncementBody,
|
||||
UpdateAnnouncementBody,
|
||||
} from '../types/announcements.types';
|
||||
|
||||
const announcementsService = new AnnouncementsService();
|
||||
|
||||
@Route('api/instructors/courses/{courseId}/announcements')
|
||||
@Tags('Announcements - Instructor')
|
||||
export class AnnouncementsController {
|
||||
|
||||
/**
|
||||
* ดึงรายการประกาศของคอร์ส
|
||||
* List announcements for a course
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
* @param page - หน้าที่ต้องการ / Page number
|
||||
* @param limit - จำนวนต่อหน้า / Items per page
|
||||
*/
|
||||
@Get()
|
||||
@SuccessResponse('200', 'Announcements retrieved successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
public async listAnnouncements(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Query() page?: number,
|
||||
@Query() limit?: number
|
||||
): Promise<ListAnnouncementResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.listAnnouncement({
|
||||
token,
|
||||
course_id: courseId,
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้างประกาศใหม่
|
||||
* Create a new announcement
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
*/
|
||||
@Post()
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Announcement created successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
public async createAnnouncement(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Body() body: CreateAnnouncementBody
|
||||
): Promise<CreateAnnouncementResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.createAnnouncement({
|
||||
token,
|
||||
course_id: courseId,
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
status: body.status,
|
||||
is_pinned: body.is_pinned,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดตประกาศ
|
||||
* Update an announcement
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
* @param announcementId - รหัสประกาศ / Announcement ID
|
||||
*/
|
||||
@Put('{announcementId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Announcement updated successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Announcement not found')
|
||||
public async updateAnnouncement(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() announcementId: number,
|
||||
@Body() body: UpdateAnnouncementBody
|
||||
): Promise<UpdateAnnouncementResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.updateAnnouncement({
|
||||
token,
|
||||
course_id: courseId,
|
||||
announcement_id: announcementId,
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
status: body.status,
|
||||
is_pinned: body.is_pinned,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบประกาศ
|
||||
* Delete an announcement
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
* @param announcementId - รหัสประกาศ / Announcement ID
|
||||
*/
|
||||
@Delete('{announcementId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Announcement deleted successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Announcement not found')
|
||||
public async deleteAnnouncement(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() announcementId: number
|
||||
): Promise<DeleteAnnouncementResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.deleteAnnouncement({
|
||||
token,
|
||||
course_id: courseId,
|
||||
announcement_id: announcementId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปโหลดไฟล์แนบ
|
||||
* Upload attachment to announcement
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
* @param announcementId - รหัสประกาศ / Announcement ID
|
||||
*/
|
||||
@Post('{announcementId}/attachments')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Attachment uploaded successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Announcement not found')
|
||||
public async uploadAttachment(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() announcementId: number,
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
): Promise<UploadAnnouncementAttachmentResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.uploadAttachment({
|
||||
token,
|
||||
course_id: courseId,
|
||||
announcement_id: announcementId,
|
||||
file: file as any,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบไฟล์แนบ
|
||||
* Delete attachment from announcement
|
||||
* @param courseId - รหัสคอร์ส / Course ID
|
||||
* @param announcementId - รหัสประกาศ / Announcement ID
|
||||
* @param attachmentId - รหัสไฟล์แนบ / Attachment ID
|
||||
*/
|
||||
@Delete('{announcementId}/attachments/{attachmentId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Attachment deleted successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Attachment not found')
|
||||
public async deleteAttachment(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() announcementId: number,
|
||||
@Path() attachmentId: number
|
||||
): Promise<DeleteAnnouncementAttachmentResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await announcementsService.deleteAttachment({
|
||||
token,
|
||||
course_id: courseId,
|
||||
announcement_id: announcementId,
|
||||
attachment_id: attachmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
import { prisma } from '../config/database';
|
||||
import { config } from '../config';
|
||||
import { logger } from '../config/logger';
|
||||
import { UnauthorizedError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import {
|
||||
ListAnnouncementResponse,
|
||||
CreateAnnouncementInput,
|
||||
CreateAnnouncementResponse,
|
||||
ListAnnouncementInput,
|
||||
UpdateAnnouncementInput,
|
||||
UpdateAnnouncementResponse,
|
||||
DeleteAnnouncementAttachmentInput,
|
||||
DeleteAnnouncementAttachmentResponse,
|
||||
UploadAnnouncementAttachmentInput,
|
||||
UploadAnnouncementAttachmentResponse,
|
||||
DeleteAnnouncementInput,
|
||||
DeleteAnnouncementResponse,
|
||||
Announcement,
|
||||
} from '../types/announcements.types';
|
||||
import { CoursesInstructorService } from './CoursesInstructor.service';
|
||||
import { uploadFile, deleteFile } from '../config/minio';
|
||||
|
||||
export class AnnouncementsService {
|
||||
|
||||
/**
|
||||
* ดึงรายการประกาศของคอร์ส (ใช้ได้ทั้ง instructor, admin, student)
|
||||
* List announcements for a course (accessible by instructor, admin, student)
|
||||
*/
|
||||
async listAnnouncement(input: ListAnnouncementInput): Promise<ListAnnouncementResponse> {
|
||||
try {
|
||||
const { token, course_id, page = 1, limit = 10 } = input;
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
|
||||
|
||||
// Check user access - instructor, admin, or enrolled student
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.id },
|
||||
include: { role: true },
|
||||
});
|
||||
if (!user) {
|
||||
throw new UnauthorizedError('Invalid token');
|
||||
}
|
||||
|
||||
// Admin can access all courses
|
||||
const isAdmin = user.role.code === 'ADMIN';
|
||||
|
||||
// Check if instructor of this course
|
||||
const isInstructor = await prisma.courseInstructor.findFirst({
|
||||
where: { course_id, user_id: decoded.id },
|
||||
});
|
||||
|
||||
// Check if enrolled student
|
||||
const isEnrolled = await prisma.enrollment.findFirst({
|
||||
where: { course_id, user_id: decoded.id },
|
||||
});
|
||||
|
||||
if (!isAdmin && !isInstructor && !isEnrolled) {
|
||||
throw new ForbiddenError('You do not have access to this course announcements');
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Students only see PUBLISHED announcements
|
||||
const statusFilter = (isAdmin || isInstructor) ? {} : { status: 'PUBLISHED' as const };
|
||||
|
||||
const [announcements, total] = await Promise.all([
|
||||
prisma.announcement.findMany({
|
||||
where: { course_id, ...statusFilter },
|
||||
include: {
|
||||
attachments: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ is_pinned: 'desc' },
|
||||
{ created_at: 'desc' },
|
||||
],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.announcement.count({ where: { course_id } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Announcements retrieved successfully',
|
||||
data: announcements.map(a => ({
|
||||
id: a.id,
|
||||
title: a.title as { th: string; en: string },
|
||||
content: a.content as { th: string; en: string },
|
||||
status: a.status,
|
||||
is_pinned: a.is_pinned,
|
||||
created_at: a.created_at,
|
||||
updated_at: a.updated_at,
|
||||
attachments: a.attachments.map(att => ({
|
||||
id: att.id,
|
||||
announcement_id: att.announcement_id,
|
||||
file_name: att.file_name,
|
||||
file_path: att.file_path,
|
||||
created_at: att.created_at,
|
||||
updated_at: att.created_at,
|
||||
})),
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error listing announcements: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้างประกาศใหม่
|
||||
* Create a new announcement
|
||||
*/
|
||||
async createAnnouncement(input: CreateAnnouncementInput): Promise<CreateAnnouncementResponse> {
|
||||
try {
|
||||
const { token, course_id, title, content, status, is_pinned } = input;
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
|
||||
// Validate instructor access
|
||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
|
||||
const announcement = await prisma.announcement.create({
|
||||
data: {
|
||||
course_id,
|
||||
title,
|
||||
content,
|
||||
status: status as any,
|
||||
is_pinned,
|
||||
published_at: status === 'PUBLISHED' ? new Date() : null,
|
||||
created_by: decoded.id,
|
||||
},
|
||||
include: {
|
||||
attachments: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
code: 201,
|
||||
message: 'Announcement created successfully',
|
||||
data: {
|
||||
id: announcement.id,
|
||||
title: announcement.title as { th: string; en: string },
|
||||
content: announcement.content as { th: string; en: string },
|
||||
status: announcement.status,
|
||||
is_pinned: announcement.is_pinned,
|
||||
created_at: announcement.created_at,
|
||||
updated_at: announcement.updated_at,
|
||||
attachments: [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error creating announcement: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดตประกาศ
|
||||
* Update an announcement
|
||||
*/
|
||||
async updateAnnouncement(input: UpdateAnnouncementInput): Promise<UpdateAnnouncementResponse> {
|
||||
try {
|
||||
const { token, course_id, announcement_id, title, content, status, is_pinned } = input;
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
|
||||
// Validate instructor access
|
||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
|
||||
// Check announcement exists and belongs to course
|
||||
const existing = await prisma.announcement.findFirst({
|
||||
where: { id: announcement_id, course_id },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Announcement not found');
|
||||
}
|
||||
|
||||
const announcement = await prisma.announcement.update({
|
||||
where: { id: announcement_id },
|
||||
data: {
|
||||
title,
|
||||
content,
|
||||
status: status as any,
|
||||
is_pinned,
|
||||
published_at: status === 'PUBLISHED' && existing.status !== 'PUBLISHED' ? new Date() : existing.published_at,
|
||||
updated_by: decoded.id,
|
||||
},
|
||||
include: {
|
||||
attachments: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Announcement updated successfully',
|
||||
data: {
|
||||
id: announcement.id,
|
||||
title: announcement.title as { th: string; en: string },
|
||||
content: announcement.content as { th: string; en: string },
|
||||
status: announcement.status,
|
||||
is_pinned: announcement.is_pinned,
|
||||
created_at: announcement.created_at,
|
||||
updated_at: announcement.updated_at,
|
||||
attachments: announcement.attachments.map(att => ({
|
||||
id: att.id,
|
||||
announcement_id: att.announcement_id,
|
||||
file_name: att.file_name,
|
||||
file_path: att.file_path,
|
||||
created_at: att.created_at,
|
||||
updated_at: att.created_at,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error updating announcement: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบประกาศ
|
||||
* Delete an announcement
|
||||
*/
|
||||
async deleteAnnouncement(input: DeleteAnnouncementInput): Promise<DeleteAnnouncementResponse> {
|
||||
try {
|
||||
const { token, course_id, announcement_id } = input;
|
||||
jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
|
||||
// Validate instructor access
|
||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
|
||||
// Check announcement exists and belongs to course
|
||||
const existing = await prisma.announcement.findFirst({
|
||||
where: { id: announcement_id, course_id },
|
||||
include: { attachments: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Announcement not found');
|
||||
}
|
||||
|
||||
// Delete attachments from storage
|
||||
for (const attachment of existing.attachments) {
|
||||
try {
|
||||
await deleteFile(attachment.file_path);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to delete attachment file: ${attachment.file_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete announcement (attachments cascade deleted)
|
||||
await prisma.announcement.delete({
|
||||
where: { id: announcement_id },
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Announcement deleted successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting announcement: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปโหลดไฟล์แนบ
|
||||
* Upload attachment to announcement
|
||||
*/
|
||||
async uploadAttachment(input: UploadAnnouncementAttachmentInput): Promise<UploadAnnouncementAttachmentResponse> {
|
||||
try {
|
||||
const { token, course_id, announcement_id, file } = input;
|
||||
jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
|
||||
// Validate instructor access
|
||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
|
||||
// Check announcement exists and belongs to course
|
||||
const existing = await prisma.announcement.findFirst({
|
||||
where: { id: announcement_id, course_id },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Announcement not found');
|
||||
}
|
||||
|
||||
// Generate file path
|
||||
const timestamp = Date.now();
|
||||
const uniqueId = Math.random().toString(36).substring(2, 15);
|
||||
const fileName = (file as any).originalname || 'file';
|
||||
const extension = fileName.split('.').pop() || '';
|
||||
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
|
||||
const filePath = `courses/${course_id}/announcements/${announcement_id}/${safeFilename}`;
|
||||
|
||||
// Upload to MinIO
|
||||
const fileBuffer = (file as any).buffer;
|
||||
const mimeType = (file as any).mimetype || 'application/octet-stream';
|
||||
const fileSize = fileBuffer.length;
|
||||
|
||||
await uploadFile(filePath, fileBuffer, mimeType);
|
||||
|
||||
// Create attachment record
|
||||
const attachment = await prisma.announcementAttachment.create({
|
||||
data: {
|
||||
announcement_id,
|
||||
file_name: fileName,
|
||||
file_path: filePath,
|
||||
file_size: fileSize,
|
||||
mime_type: mimeType,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
code: 201,
|
||||
message: 'Attachment uploaded successfully',
|
||||
data: {
|
||||
id: attachment.id,
|
||||
announcement_id: attachment.announcement_id,
|
||||
file_name: attachment.file_name,
|
||||
file_path: attachment.file_path,
|
||||
created_at: attachment.created_at,
|
||||
updated_at: attachment.created_at,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error uploading attachment: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบไฟล์แนบ
|
||||
* Delete attachment from announcement
|
||||
*/
|
||||
async deleteAttachment(input: DeleteAnnouncementAttachmentInput): Promise<DeleteAnnouncementAttachmentResponse> {
|
||||
try {
|
||||
const { token, course_id, announcement_id, attachment_id } = input;
|
||||
jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
|
||||
// Validate instructor access
|
||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
|
||||
// Check attachment exists and belongs to announcement in this course
|
||||
const attachment = await prisma.announcementAttachment.findFirst({
|
||||
where: {
|
||||
id: attachment_id,
|
||||
announcement_id,
|
||||
announcement: { course_id },
|
||||
},
|
||||
});
|
||||
|
||||
if (!attachment) {
|
||||
throw new NotFoundError('Attachment not found');
|
||||
}
|
||||
|
||||
// Delete from storage
|
||||
try {
|
||||
await deleteFile(attachment.file_path);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to delete attachment file: ${attachment.file_path}`);
|
||||
}
|
||||
|
||||
// Delete record
|
||||
await prisma.announcementAttachment.delete({
|
||||
where: { id: attachment_id },
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Attachment deleted successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting attachment: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
import { MultiLanguageText } from './index';
|
||||
|
||||
// Use MultiLanguageText from index.ts for consistency
|
||||
export type MultiLangText = MultiLanguageText;
|
||||
|
||||
export interface Announcement {
|
||||
id: number;
|
||||
title: MultiLangText;
|
||||
content: MultiLangText;
|
||||
title: MultiLanguageText;
|
||||
content: MultiLanguageText;
|
||||
status: string;
|
||||
is_pinned: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
updated_at: Date | null;
|
||||
attachments: AnnouncementAttachment[];
|
||||
}
|
||||
|
||||
|
|
@ -42,15 +39,83 @@ export interface ListAnnouncementInput{
|
|||
export interface CreateAnnouncementInput{
|
||||
token: string;
|
||||
course_id: number;
|
||||
title: MultiLangText;
|
||||
content: MultiLangText;
|
||||
title: MultiLanguageText;
|
||||
content: MultiLanguageText;
|
||||
status: string;
|
||||
is_pinned: boolean;
|
||||
attachments?: AnnouncementAttachment[];
|
||||
}
|
||||
|
||||
export interface UploadAnnouncementAttachmentInput{
|
||||
token: string;
|
||||
course_id: number;
|
||||
announcement_id: number;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface UploadAnnouncementAttachmentResponse{
|
||||
code: number;
|
||||
message: string;
|
||||
data: AnnouncementAttachment;
|
||||
}
|
||||
|
||||
export interface DeleteAnnouncementAttachmentInput{
|
||||
token: string;
|
||||
course_id: number;
|
||||
announcement_id: number;
|
||||
attachment_id: number;
|
||||
}
|
||||
|
||||
export interface DeleteAnnouncementAttachmentResponse{
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CreateAnnouncementResponse{
|
||||
code: number;
|
||||
message: string;
|
||||
data: Announcement;
|
||||
}
|
||||
|
||||
export interface UpdateAnnouncementInput{
|
||||
token: string;
|
||||
course_id: number;
|
||||
announcement_id: number;
|
||||
title: MultiLanguageText;
|
||||
content: MultiLanguageText;
|
||||
status: string;
|
||||
is_pinned: boolean;
|
||||
attachments?: AnnouncementAttachment[];
|
||||
}
|
||||
|
||||
export interface UpdateAnnouncementResponse{
|
||||
code: number;
|
||||
message: string;
|
||||
data: Announcement;
|
||||
}
|
||||
|
||||
export interface DeleteAnnouncementInput{
|
||||
token: string;
|
||||
course_id: number;
|
||||
announcement_id: number;
|
||||
}
|
||||
|
||||
export interface DeleteAnnouncementResponse{
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Body types for TSOA controller
|
||||
export interface CreateAnnouncementBody {
|
||||
title: MultiLanguageText;
|
||||
content: MultiLanguageText;
|
||||
status: string;
|
||||
is_pinned: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAnnouncementBody {
|
||||
title: MultiLanguageText;
|
||||
content: MultiLanguageText;
|
||||
status: string;
|
||||
is_pinned: boolean;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue