From 05755992a72f1b5ad5a339ab992449e8a02821ba Mon Sep 17 00:00:00 2001 From: JakkrapartXD Date: Wed, 4 Feb 2026 16:15:38 +0700 Subject: [PATCH] feat: add published_at field support to announcements with scheduled publishing and student visibility filtering Add published_at field to announcement creation and updates, allowing instructors to schedule announcement publishing. Update student announcement filtering to only show PUBLISHED announcements where published_at <= now. Modify announcement creation to set published_at to provided value or current time for PUBLISHED status. Update announcement updates to handle published_at changes while --- .../controllers/announcementsController.ts | 2 + Backend/src/services/announcements.service.ts | 42 +++++++++++++++---- Backend/src/types/announcements.types.ts | 5 +++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Backend/src/controllers/announcementsController.ts b/Backend/src/controllers/announcementsController.ts index 8c5d855d..6a4b901c 100644 --- a/Backend/src/controllers/announcementsController.ts +++ b/Backend/src/controllers/announcementsController.ts @@ -75,6 +75,7 @@ export class AnnouncementsController { content: parsed.content, status: parsed.status, is_pinned: parsed.is_pinned, + published_at: parsed.published_at ? new Date(parsed.published_at) : undefined, files, }); } @@ -107,6 +108,7 @@ export class AnnouncementsController { content: body.content, status: body.status, is_pinned: body.is_pinned, + published_at: body.published_at ? new Date(body.published_at) : undefined, }); } diff --git a/Backend/src/services/announcements.service.ts b/Backend/src/services/announcements.service.ts index 896c999e..eb26b3d1 100644 --- a/Backend/src/services/announcements.service.ts +++ b/Backend/src/services/announcements.service.ts @@ -58,12 +58,19 @@ export class AnnouncementsService { const skip = (page - 1) * limit; - // Students only see PUBLISHED announcements - const statusFilter = (isAdmin || isInstructor) ? {} : { status: 'PUBLISHED' as const }; + // Students only see PUBLISHED announcements with published_at <= now + const now = new Date(); + const whereClause: any = { course_id }; + + if (!(isAdmin || isInstructor)) { + // Students: only show PUBLISHED and published_at <= now + whereClause.status = 'PUBLISHED'; + whereClause.published_at = { lte: now }; + } const [announcements, total] = await Promise.all([ prisma.announcement.findMany({ - where: { course_id, ...statusFilter }, + where: whereClause, include: { attachments: true, }, @@ -74,7 +81,7 @@ export class AnnouncementsService { skip, take: limit, }), - prisma.announcement.count({ where: { course_id } }), + prisma.announcement.count({ where: whereClause }), ]); // Generate presigned URLs for attachments @@ -105,6 +112,7 @@ export class AnnouncementsService { content: a.content as { th: string; en: string }, status: a.status, is_pinned: a.is_pinned, + published_at: a.published_at, created_at: a.created_at, updated_at: a.updated_at, attachments: attachmentsWithUrls, @@ -132,12 +140,18 @@ export class AnnouncementsService { */ async createAnnouncement(input: CreateAnnouncementInput): Promise { try { - const { token, course_id, title, content, status, is_pinned, files } = input; + const { token, course_id, title, content, status, is_pinned, published_at, files } = input; const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access await CoursesInstructorService.validateCourseInstructor(token, course_id); + // Determine published_at: use provided value or default to now if status is PUBLISHED + let finalPublishedAt: Date | null = null; + if (status === 'PUBLISHED') { + finalPublishedAt = published_at ? new Date(published_at) : new Date(); + } + // Create announcement const announcement = await prisma.announcement.create({ data: { @@ -146,7 +160,7 @@ export class AnnouncementsService { content, status: status as any, is_pinned, - published_at: status === 'PUBLISHED' ? new Date() : null, + published_at: finalPublishedAt, created_by: decoded.id, }, }); @@ -204,6 +218,7 @@ export class AnnouncementsService { content: announcement.content as { th: string; en: string }, status: announcement.status, is_pinned: announcement.is_pinned, + published_at: announcement.published_at, created_at: announcement.created_at, updated_at: announcement.updated_at, attachments, @@ -221,7 +236,7 @@ export class AnnouncementsService { */ async updateAnnouncement(input: UpdateAnnouncementInput): Promise { try { - const { token, course_id, announcement_id, title, content, status, is_pinned } = input; + const { token, course_id, announcement_id, title, content, status, is_pinned, published_at } = input; const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access @@ -236,6 +251,16 @@ export class AnnouncementsService { throw new NotFoundError('Announcement not found'); } + // Determine published_at + let finalPublishedAt: Date | null = existing.published_at; + if (status === 'PUBLISHED') { + if (published_at) { + finalPublishedAt = new Date(published_at); + } else if (existing.status !== 'PUBLISHED') { + finalPublishedAt = new Date(); + } + } + const announcement = await prisma.announcement.update({ where: { id: announcement_id }, data: { @@ -243,7 +268,7 @@ export class AnnouncementsService { content, status: status as any, is_pinned, - published_at: status === 'PUBLISHED' && existing.status !== 'PUBLISHED' ? new Date() : existing.published_at, + published_at: finalPublishedAt, updated_by: decoded.id, }, include: { @@ -260,6 +285,7 @@ export class AnnouncementsService { content: announcement.content as { th: string; en: string }, status: announcement.status, is_pinned: announcement.is_pinned, + published_at: announcement.published_at, created_at: announcement.created_at, updated_at: announcement.updated_at, attachments: announcement.attachments.map(att => ({ diff --git a/Backend/src/types/announcements.types.ts b/Backend/src/types/announcements.types.ts index 7aa8db33..f960a961 100644 --- a/Backend/src/types/announcements.types.ts +++ b/Backend/src/types/announcements.types.ts @@ -6,6 +6,7 @@ export interface Announcement { content: MultiLanguageText; status: string; is_pinned: boolean; + published_at: Date | null; created_at: Date; updated_at: Date | null; attachments: AnnouncementAttachment[]; @@ -44,6 +45,7 @@ export interface CreateAnnouncementInput{ content: MultiLanguageText; status: string; is_pinned: boolean; + published_at?: Date; files?: Express.Multer.File[]; } @@ -86,6 +88,7 @@ export interface UpdateAnnouncementInput{ content: MultiLanguageText; status: string; is_pinned: boolean; + published_at?: Date; attachments?: AnnouncementAttachment[]; } @@ -112,6 +115,7 @@ export interface CreateAnnouncementBody { content: MultiLanguageText; status: string; is_pinned: boolean; + published_at?: string; } export interface UpdateAnnouncementBody { @@ -119,4 +123,5 @@ export interface UpdateAnnouncementBody { content: MultiLanguageText; status: string; is_pinned: boolean; + published_at?: string; } \ No newline at end of file