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
This commit is contained in:
parent
67f10c4287
commit
05755992a7
3 changed files with 41 additions and 8 deletions
|
|
@ -75,6 +75,7 @@ export class AnnouncementsController {
|
||||||
content: parsed.content,
|
content: parsed.content,
|
||||||
status: parsed.status,
|
status: parsed.status,
|
||||||
is_pinned: parsed.is_pinned,
|
is_pinned: parsed.is_pinned,
|
||||||
|
published_at: parsed.published_at ? new Date(parsed.published_at) : undefined,
|
||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +108,7 @@ export class AnnouncementsController {
|
||||||
content: body.content,
|
content: body.content,
|
||||||
status: body.status,
|
status: body.status,
|
||||||
is_pinned: body.is_pinned,
|
is_pinned: body.is_pinned,
|
||||||
|
published_at: body.published_at ? new Date(body.published_at) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,19 @@ export class AnnouncementsService {
|
||||||
|
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
// Students only see PUBLISHED announcements
|
// Students only see PUBLISHED announcements with published_at <= now
|
||||||
const statusFilter = (isAdmin || isInstructor) ? {} : { status: 'PUBLISHED' as const };
|
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([
|
const [announcements, total] = await Promise.all([
|
||||||
prisma.announcement.findMany({
|
prisma.announcement.findMany({
|
||||||
where: { course_id, ...statusFilter },
|
where: whereClause,
|
||||||
include: {
|
include: {
|
||||||
attachments: true,
|
attachments: true,
|
||||||
},
|
},
|
||||||
|
|
@ -74,7 +81,7 @@ export class AnnouncementsService {
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
}),
|
}),
|
||||||
prisma.announcement.count({ where: { course_id } }),
|
prisma.announcement.count({ where: whereClause }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Generate presigned URLs for attachments
|
// Generate presigned URLs for attachments
|
||||||
|
|
@ -105,6 +112,7 @@ export class AnnouncementsService {
|
||||||
content: a.content as { th: string; en: string },
|
content: a.content as { th: string; en: string },
|
||||||
status: a.status,
|
status: a.status,
|
||||||
is_pinned: a.is_pinned,
|
is_pinned: a.is_pinned,
|
||||||
|
published_at: a.published_at,
|
||||||
created_at: a.created_at,
|
created_at: a.created_at,
|
||||||
updated_at: a.updated_at,
|
updated_at: a.updated_at,
|
||||||
attachments: attachmentsWithUrls,
|
attachments: attachmentsWithUrls,
|
||||||
|
|
@ -132,12 +140,18 @@ export class AnnouncementsService {
|
||||||
*/
|
*/
|
||||||
async createAnnouncement(input: CreateAnnouncementInput): Promise<CreateAnnouncementResponse> {
|
async createAnnouncement(input: CreateAnnouncementInput): Promise<CreateAnnouncementResponse> {
|
||||||
try {
|
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 };
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||||
|
|
||||||
// Validate instructor access
|
// Validate instructor access
|
||||||
await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
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
|
// Create announcement
|
||||||
const announcement = await prisma.announcement.create({
|
const announcement = await prisma.announcement.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -146,7 +160,7 @@ export class AnnouncementsService {
|
||||||
content,
|
content,
|
||||||
status: status as any,
|
status: status as any,
|
||||||
is_pinned,
|
is_pinned,
|
||||||
published_at: status === 'PUBLISHED' ? new Date() : null,
|
published_at: finalPublishedAt,
|
||||||
created_by: decoded.id,
|
created_by: decoded.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -204,6 +218,7 @@ export class AnnouncementsService {
|
||||||
content: announcement.content as { th: string; en: string },
|
content: announcement.content as { th: string; en: string },
|
||||||
status: announcement.status,
|
status: announcement.status,
|
||||||
is_pinned: announcement.is_pinned,
|
is_pinned: announcement.is_pinned,
|
||||||
|
published_at: announcement.published_at,
|
||||||
created_at: announcement.created_at,
|
created_at: announcement.created_at,
|
||||||
updated_at: announcement.updated_at,
|
updated_at: announcement.updated_at,
|
||||||
attachments,
|
attachments,
|
||||||
|
|
@ -221,7 +236,7 @@ export class AnnouncementsService {
|
||||||
*/
|
*/
|
||||||
async updateAnnouncement(input: UpdateAnnouncementInput): Promise<UpdateAnnouncementResponse> {
|
async updateAnnouncement(input: UpdateAnnouncementInput): Promise<UpdateAnnouncementResponse> {
|
||||||
try {
|
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 };
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||||
|
|
||||||
// Validate instructor access
|
// Validate instructor access
|
||||||
|
|
@ -236,6 +251,16 @@ export class AnnouncementsService {
|
||||||
throw new NotFoundError('Announcement not found');
|
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({
|
const announcement = await prisma.announcement.update({
|
||||||
where: { id: announcement_id },
|
where: { id: announcement_id },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -243,7 +268,7 @@ export class AnnouncementsService {
|
||||||
content,
|
content,
|
||||||
status: status as any,
|
status: status as any,
|
||||||
is_pinned,
|
is_pinned,
|
||||||
published_at: status === 'PUBLISHED' && existing.status !== 'PUBLISHED' ? new Date() : existing.published_at,
|
published_at: finalPublishedAt,
|
||||||
updated_by: decoded.id,
|
updated_by: decoded.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -260,6 +285,7 @@ export class AnnouncementsService {
|
||||||
content: announcement.content as { th: string; en: string },
|
content: announcement.content as { th: string; en: string },
|
||||||
status: announcement.status,
|
status: announcement.status,
|
||||||
is_pinned: announcement.is_pinned,
|
is_pinned: announcement.is_pinned,
|
||||||
|
published_at: announcement.published_at,
|
||||||
created_at: announcement.created_at,
|
created_at: announcement.created_at,
|
||||||
updated_at: announcement.updated_at,
|
updated_at: announcement.updated_at,
|
||||||
attachments: announcement.attachments.map(att => ({
|
attachments: announcement.attachments.map(att => ({
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export interface Announcement {
|
||||||
content: MultiLanguageText;
|
content: MultiLanguageText;
|
||||||
status: string;
|
status: string;
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
|
published_at: Date | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date | null;
|
updated_at: Date | null;
|
||||||
attachments: AnnouncementAttachment[];
|
attachments: AnnouncementAttachment[];
|
||||||
|
|
@ -44,6 +45,7 @@ export interface CreateAnnouncementInput{
|
||||||
content: MultiLanguageText;
|
content: MultiLanguageText;
|
||||||
status: string;
|
status: string;
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
|
published_at?: Date;
|
||||||
files?: Express.Multer.File[];
|
files?: Express.Multer.File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +88,7 @@ export interface UpdateAnnouncementInput{
|
||||||
content: MultiLanguageText;
|
content: MultiLanguageText;
|
||||||
status: string;
|
status: string;
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
|
published_at?: Date;
|
||||||
attachments?: AnnouncementAttachment[];
|
attachments?: AnnouncementAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,6 +115,7 @@ export interface CreateAnnouncementBody {
|
||||||
content: MultiLanguageText;
|
content: MultiLanguageText;
|
||||||
status: string;
|
status: string;
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
|
published_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateAnnouncementBody {
|
export interface UpdateAnnouncementBody {
|
||||||
|
|
@ -119,4 +123,5 @@ export interface UpdateAnnouncementBody {
|
||||||
content: MultiLanguageText;
|
content: MultiLanguageText;
|
||||||
status: string;
|
status: string;
|
||||||
is_pinned: boolean;
|
is_pinned: boolean;
|
||||||
|
published_at?: string;
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue