observatory

This commit is contained in:
alma 2025-05-04 21:31:09 +02:00
parent e9142b28de
commit 3e3653183a
7 changed files with 319 additions and 81 deletions

View File

@ -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 });
}
}

View File

@ -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 });
}
}

View File

@ -4,7 +4,10 @@ export interface Announcement {
content: string; content: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
author: string;
authorId: string; authorId: string;
targetRoles: string[]; targetRoles: string[];
author: {
id: string;
email: string;
};
} }

View File

@ -32,7 +32,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { CheckIcon, Loader2 } from "lucide-react"; import { CheckIcon, Loader2 } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Announcement } from "@/app/types/announcement"; import { useToast } from "@/components/ui/use-toast";
// Form schema // Form schema
const formSchema = z.object({ const formSchema = z.object({
@ -49,6 +49,7 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) {
const [selectedRoles, setSelectedRoles] = useState<string[]>([]); const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false); const [isSuccess, setIsSuccess] = useState(false);
const { toast } = useToast();
// Initialize form // Initialize form
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@ -100,21 +101,38 @@ export function AnnouncementForm({ userRole }: AnnouncementFormProps) {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// In a real implementation, this would be an API call // Send the data to the API
console.log("Announcement data:", data); const response = await fetch('/api/announcements', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
// Simulate API delay if (!response.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); throw new Error('Failed to create announcement');
}
// Reset form and show success message // Reset form and show success message
form.reset(); form.reset();
setSelectedRoles([]); setSelectedRoles([]);
setIsSuccess(true); setIsSuccess(true);
toast({
title: "Announcement created",
description: "The announcement has been created successfully.",
});
// Hide success message after a delay // Hide success message after a delay
setTimeout(() => setIsSuccess(false), 3000); setTimeout(() => setIsSuccess(false), 3000);
} catch (error) { } catch (error) {
console.error("Error submitting announcement:", error); console.error("Error submitting announcement:", error);
toast({
title: "Error",
description: "Failed to create the announcement. Please try again.",
variant: "destructive",
});
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }

View File

@ -11,43 +11,38 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Announcement } from "@/app/types/announcement"; 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() { export function AnnouncementsDropdown() {
const [announcements, setAnnouncements] = useState<Announcement[]>([]); const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null); const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
// In a real implementation, this would be an API call // Fetch announcements from the API
// For now, using mock data const fetchAnnouncements = async () => {
setAnnouncements(mockAnnouncements); try {
if (mockAnnouncements.length > 0) { setLoading(true);
setSelectedAnnouncement(mockAnnouncements[0]); const response = await fetch('/api/announcements');
}
setLoading(false); 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) => { const handleAnnouncementChange = (announcementId: string) => {
@ -63,6 +58,10 @@ export function AnnouncementsDropdown() {
<div className="flex items-center justify-center h-40"> <div className="flex items-center justify-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div> </div>
) : error ? (
<div className="text-center py-10 text-red-500">
{error}
</div>
) : announcements.length === 0 ? ( ) : announcements.length === 0 ? (
<div className="text-center py-10 text-gray-500"> <div className="text-center py-10 text-gray-500">
No announcements available No announcements available
@ -92,7 +91,7 @@ export function AnnouncementsDropdown() {
<CardHeader> <CardHeader>
<CardTitle>{selectedAnnouncement.title}</CardTitle> <CardTitle>{selectedAnnouncement.title}</CardTitle>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Posted by {selectedAnnouncement.author} on {new Date(selectedAnnouncement.createdAt).toLocaleDateString()} Posted by {selectedAnnouncement.author.email} on {new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@ -38,40 +38,7 @@ import {
DialogTrigger DialogTrigger
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Announcement } from "@/app/types/announcement"; import { Announcement } from "@/app/types/announcement";
import { useToast } from "@/components/ui/use-toast";
// 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"]
}
];
interface AnnouncementsListProps { interface AnnouncementsListProps {
userRole: string[]; userRole: string[];
@ -84,12 +51,32 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false); const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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(() => { useEffect(() => {
// In a real implementation, this would be an API call fetchAnnouncements();
// For now, using mock data
setAnnouncements(mockAnnouncements);
setLoading(false);
}, []); }, []);
// Filter announcements based on search term // Filter announcements based on search term
@ -111,11 +98,33 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
setIsDeleteDialogOpen(true); setIsDeleteDialogOpen(true);
}; };
const confirmDelete = () => { const confirmDelete = async () => {
if (selectedAnnouncement) { if (!selectedAnnouncement) return;
// In a real implementation, this would be an API call
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)); setAnnouncements(announcements.filter(a => a.id !== selectedAnnouncement.id));
setIsDeleteDialogOpen(false); 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) {
<div className="flex items-center justify-center h-40"> <div className="flex items-center justify-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div> </div>
) : error ? (
<div className="text-center py-10 text-red-500">
{error}
<Button onClick={fetchAnnouncements} className="ml-4">Retry</Button>
</div>
) : filteredAnnouncements.length === 0 ? ( ) : filteredAnnouncements.length === 0 ? (
<div className="text-center py-10 text-gray-500"> <div className="text-center py-10 text-gray-500">
No announcements found No announcements found
@ -186,7 +200,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
<TableRow key={announcement.id}> <TableRow key={announcement.id}>
<TableCell className="font-medium">{announcement.title}</TableCell> <TableCell className="font-medium">{announcement.title}</TableCell>
<TableCell>{new Date(announcement.createdAt).toLocaleDateString()}</TableCell> <TableCell>{new Date(announcement.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{announcement.author}</TableCell> <TableCell>{announcement.author.email}</TableCell>
<TableCell>{formatRoles(announcement.targetRoles)}</TableCell> <TableCell>{formatRoles(announcement.targetRoles)}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
@ -221,7 +235,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
<DialogHeader> <DialogHeader>
<DialogTitle>{selectedAnnouncement?.title}</DialogTitle> <DialogTitle>{selectedAnnouncement?.title}</DialogTitle>
<DialogDescription> <DialogDescription>
Posted by {selectedAnnouncement?.author} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()} Posted by {selectedAnnouncement?.author.email} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="mt-4"> <div className="mt-4">

View File

@ -21,6 +21,7 @@ model User {
events Event[] events Event[]
mailCredentials MailCredentials[] mailCredentials MailCredentials[]
webdavCredentials WebDAVCredentials? webdavCredentials WebDAVCredentials?
announcements Announcement[]
} }
model Calendar { model Calendar {
@ -98,4 +99,17 @@ model WebDAVCredentials {
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId]) @@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])
} }