diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts new file mode 100644 index 00000000..a400878b --- /dev/null +++ b/app/api/announcements/[id]/route.ts @@ -0,0 +1,100 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { prisma } from "@/lib/prisma"; + +// GET - Retrieve a specific announcement +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = params; + + // Find announcement by ID + const announcement = await prisma.announcement.findUnique({ + where: { id }, + include: { + author: { + select: { + id: true, + email: true + } + } + } + }); + + if (!announcement) { + return NextResponse.json({ error: "Announcement not found" }, { status: 404 }); + } + + // Check if user has access to this announcement + const userRole = session.user.role || []; + const roles = Array.isArray(userRole) ? userRole : [userRole]; + + const hasAccess = + announcement.targetRoles.includes("all") || + announcement.targetRoles.some(role => roles.includes(role)); + + if (!hasAccess) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + return NextResponse.json(announcement); + } catch (error) { + console.error("Error fetching announcement:", error); + return NextResponse.json({ error: "Failed to fetch announcement" }, { status: 500 }); + } +} + +// DELETE - Remove an announcement +export async function DELETE( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if user has admin, entrepreneurship, or communication role + const userRole = session.user.role || []; + const roles = Array.isArray(userRole) ? userRole : [userRole]; + const hasAdminAccess = roles.some(role => + ["admin", "entrepreneurship", "communication"].includes(role) + ); + + if (!hasAdminAccess) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { id } = params; + + // Check if announcement exists + const announcement = await prisma.announcement.findUnique({ + where: { id } + }); + + if (!announcement) { + return NextResponse.json({ error: "Announcement not found" }, { status: 404 }); + } + + // Delete the announcement + await prisma.announcement.delete({ + where: { id } + }); + + return NextResponse.json({ message: "Announcement deleted successfully" }); + } catch (error) { + console.error("Error deleting announcement:", error); + return NextResponse.json({ error: "Failed to delete announcement" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/announcements/route.ts b/app/api/announcements/route.ts new file mode 100644 index 00000000..4c366973 --- /dev/null +++ b/app/api/announcements/route.ts @@ -0,0 +1,90 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { prisma } from "@/lib/prisma"; + +// GET - Retrieve all announcements (with role filtering) +export async function GET(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Get user role from session + const userRole = session.user.role || []; + const roles = Array.isArray(userRole) ? userRole : [userRole]; + + // Query announcements based on role + const announcements = await prisma.announcement.findMany({ + where: { + OR: [ + { targetRoles: { has: "all" } }, + { targetRoles: { hasSome: roles } } + ] + }, + orderBy: { + createdAt: "desc" + }, + include: { + author: { + select: { + id: true, + email: true + } + } + } + }); + + return NextResponse.json(announcements); + } catch (error) { + console.error("Error fetching announcements:", error); + return NextResponse.json({ error: "Failed to fetch announcements" }, { status: 500 }); + } +} + +// POST - Create a new announcement +export async function POST(req: NextRequest) { + try { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if user has admin, entrepreneurship, or communication role + const userRole = session.user.role || []; + const roles = Array.isArray(userRole) ? userRole : [userRole]; + const hasAdminAccess = roles.some(role => + ["admin", "entrepreneurship", "communication"].includes(role) + ); + + if (!hasAdminAccess) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + // Parse request body + const { title, content, targetRoles } = await req.json(); + + // Validate request body + if (!title || !content || !targetRoles || !targetRoles.length) { + return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); + } + + // Create new announcement + const announcement = await prisma.announcement.create({ + data: { + title, + content, + targetRoles, + authorId: session.user.id + } + }); + + return NextResponse.json(announcement, { status: 201 }); + } catch (error) { + console.error("Error creating announcement:", error); + return NextResponse.json({ error: "Failed to create announcement" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/types/announcement.ts b/app/types/announcement.ts index cdb10e5e..b7f3668f 100644 --- a/app/types/announcement.ts +++ b/app/types/announcement.ts @@ -4,7 +4,10 @@ export interface Announcement { content: string; createdAt: string; updatedAt: string; - author: string; authorId: string; targetRoles: string[]; + author: { + id: string; + email: string; + }; } \ No newline at end of file diff --git a/components/announcement/announcement-form.tsx b/components/announcement/announcement-form.tsx index 82bde590..a74c070e 100644 --- a/components/announcement/announcement-form.tsx +++ b/components/announcement/announcement-form.tsx @@ -32,7 +32,7 @@ import { } from "@/components/ui/card"; import { CheckIcon, Loader2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { Announcement } from "@/app/types/announcement"; +import { useToast } from "@/components/ui/use-toast"; // Form schema const formSchema = z.object({ @@ -49,6 +49,7 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) { const [selectedRoles, setSelectedRoles] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); + const { toast } = useToast(); // Initialize form const form = useForm>({ @@ -100,21 +101,38 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) { setIsSubmitting(true); try { - // In a real implementation, this would be an API call - console.log("Announcement data:", data); + // Send the data to the API + const response = await fetch('/api/announcements', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 1000)); + if (!response.ok) { + throw new Error('Failed to create announcement'); + } // Reset form and show success message form.reset(); setSelectedRoles([]); setIsSuccess(true); + toast({ + title: "Announcement created", + description: "The announcement has been created successfully.", + }); + // Hide success message after a delay setTimeout(() => setIsSuccess(false), 3000); } catch (error) { console.error("Error submitting announcement:", error); + toast({ + title: "Error", + description: "Failed to create the announcement. Please try again.", + variant: "destructive", + }); } finally { setIsSubmitting(false); } diff --git a/components/announcement/announcements-dropdown.tsx b/components/announcement/announcements-dropdown.tsx index a6e07b09..d63e36b7 100644 --- a/components/announcement/announcements-dropdown.tsx +++ b/components/announcement/announcements-dropdown.tsx @@ -11,43 +11,38 @@ import { import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Announcement } from "@/app/types/announcement"; -// Mock data for demo purposes -const mockAnnouncements: Announcement[] = [ - { - id: "1", - title: "System Maintenance", - content: "The system will be undergoing maintenance on Saturday from 2-4am.", - createdAt: "2023-06-01T10:00:00Z", - updatedAt: "2023-06-01T10:00:00Z", - author: "System Admin", - authorId: "admin1", - targetRoles: ["all"] - }, - { - id: "2", - title: "New Feature Launch", - content: "We're excited to announce our new collaborative workspace feature launching next week!", - createdAt: "2023-06-02T14:30:00Z", - updatedAt: "2023-06-02T14:30:00Z", - author: "Product Team", - authorId: "product1", - targetRoles: ["admin", "entrepreneurship"] - } -]; - export function AnnouncementsDropdown() { const [announcements, setAnnouncements] = useState([]); const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - // In a real implementation, this would be an API call - // For now, using mock data - setAnnouncements(mockAnnouncements); - if (mockAnnouncements.length > 0) { - setSelectedAnnouncement(mockAnnouncements[0]); - } - setLoading(false); + // Fetch announcements from the API + const fetchAnnouncements = async () => { + try { + setLoading(true); + const response = await fetch('/api/announcements'); + + if (!response.ok) { + throw new Error('Failed to fetch announcements'); + } + + const data = await response.json(); + setAnnouncements(data); + + if (data.length > 0) { + setSelectedAnnouncement(data[0]); + } + } catch (err) { + console.error('Error fetching announcements:', err); + setError('Failed to load announcements'); + } finally { + setLoading(false); + } + }; + + fetchAnnouncements(); }, []); const handleAnnouncementChange = (announcementId: string) => { @@ -63,6 +58,10 @@ export function AnnouncementsDropdown() {
+ ) : error ? ( +
+ {error} +
) : announcements.length === 0 ? (
No announcements available @@ -92,7 +91,7 @@ export function AnnouncementsDropdown() { {selectedAnnouncement.title}
- Posted by {selectedAnnouncement.author} on {new Date(selectedAnnouncement.createdAt).toLocaleDateString()} + Posted by {selectedAnnouncement.author.email} on {new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
diff --git a/components/announcement/announcements-list.tsx b/components/announcement/announcements-list.tsx index 99672cce..5f189f23 100644 --- a/components/announcement/announcements-list.tsx +++ b/components/announcement/announcements-list.tsx @@ -38,40 +38,7 @@ import { DialogTrigger } from "@/components/ui/dialog"; import { Announcement } from "@/app/types/announcement"; - -// Mock data for demo purposes -const mockAnnouncements: Announcement[] = [ - { - id: "1", - title: "System Maintenance", - content: "The system will be undergoing maintenance on Saturday from 2-4am.", - createdAt: "2023-06-01T10:00:00Z", - updatedAt: "2023-06-01T10:00:00Z", - author: "System Admin", - authorId: "admin1", - targetRoles: ["all"] - }, - { - id: "2", - title: "New Feature Launch", - content: "We're excited to announce our new collaborative workspace feature launching next week!", - createdAt: "2023-06-02T14:30:00Z", - updatedAt: "2023-06-02T14:30:00Z", - author: "Product Team", - authorId: "product1", - targetRoles: ["admin", "entrepreneurship"] - }, - { - id: "3", - title: "Team Meeting", - content: "There will be a team meeting on Monday at 10 AM to discuss the upcoming project milestones.", - createdAt: "2023-06-03T09:15:00Z", - updatedAt: "2023-06-03T09:15:00Z", - author: "Team Lead", - authorId: "lead1", - targetRoles: ["communication", "admin"] - } -]; +import { useToast } from "@/components/ui/use-toast"; interface AnnouncementsListProps { userRole: string[]; @@ -84,12 +51,32 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { const [isViewDialogOpen, setIsViewDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + // Fetch announcements + const fetchAnnouncements = async () => { + try { + setLoading(true); + const response = await fetch('/api/announcements'); + + if (!response.ok) { + throw new Error('Failed to fetch announcements'); + } + + const data = await response.json(); + setAnnouncements(data); + setError(null); + } catch (err) { + console.error('Error fetching announcements:', err); + setError('Failed to load announcements'); + } finally { + setLoading(false); + } + }; useEffect(() => { - // In a real implementation, this would be an API call - // For now, using mock data - setAnnouncements(mockAnnouncements); - setLoading(false); + fetchAnnouncements(); }, []); // Filter announcements based on search term @@ -111,11 +98,33 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { setIsDeleteDialogOpen(true); }; - const confirmDelete = () => { - if (selectedAnnouncement) { - // In a real implementation, this would be an API call + const confirmDelete = async () => { + if (!selectedAnnouncement) return; + + try { + const response = await fetch(`/api/announcements/${selectedAnnouncement.id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete announcement'); + } + + // Update the local state setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id)); setIsDeleteDialogOpen(false); + + toast({ + title: "Announcement deleted", + description: "The announcement has been deleted successfully.", + }); + } catch (err) { + console.error('Error deleting announcement:', err); + toast({ + title: "Error", + description: "Failed to delete the announcement. Please try again.", + variant: "destructive", + }); } }; @@ -165,6 +174,11 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
+ ) : error ? ( +
+ {error} + +
) : filteredAnnouncements.length === 0 ? (
No announcements found @@ -186,7 +200,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { {announcement.title} {new Date(announcement.createdAt).toLocaleDateString()} - {announcement.author} + {announcement.author.email} {formatRoles(announcement.targetRoles)}
@@ -221,7 +235,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) { {selectedAnnouncement?.title} - Posted by {selectedAnnouncement?.author} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()} + Posted by {selectedAnnouncement?.author.email} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 953c946e..e74f0f40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model User { events Event[] mailCredentials MailCredentials[] webdavCredentials WebDAVCredentials? + announcements Announcement[] } model Calendar { @@ -98,4 +99,17 @@ model WebDAVCredentials { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) +} + +model Announcement { + id String @id @default(uuid()) + title String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String + targetRoles String[] + + @@index([authorId]) } \ No newline at end of file