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;
createdAt: string;
updatedAt: string;
author: string;
authorId: string;
targetRoles: string[];
author: {
id: string;
email: string;
};
}

View File

@ -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<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const { toast } = useToast();
// Initialize form
const form = useForm<z.infer<typeof formSchema>>({
@ -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);
}

View File

@ -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<Announcement[]>([]);
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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() {
<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>
) : error ? (
<div className="text-center py-10 text-red-500">
{error}
</div>
) : announcements.length === 0 ? (
<div className="text-center py-10 text-gray-500">
No announcements available
@ -92,7 +91,7 @@ export function AnnouncementsDropdown() {
<CardHeader>
<CardTitle>{selectedAnnouncement.title}</CardTitle>
<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>
</CardHeader>
<CardContent>

View File

@ -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<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(() => {
// 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) {
<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>
) : error ? (
<div className="text-center py-10 text-red-500">
{error}
<Button onClick={fetchAnnouncements} className="ml-4">Retry</Button>
</div>
) : filteredAnnouncements.length === 0 ? (
<div className="text-center py-10 text-gray-500">
No announcements found
@ -186,7 +200,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
<TableRow key={announcement.id}>
<TableCell className="font-medium">{announcement.title}</TableCell>
<TableCell>{new Date(announcement.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{announcement.author}</TableCell>
<TableCell>{announcement.author.email}</TableCell>
<TableCell>{formatRoles(announcement.targetRoles)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
@ -221,7 +235,7 @@ export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
<DialogHeader>
<DialogTitle>{selectedAnnouncement?.title}</DialogTitle>
<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>
</DialogHeader>
<div className="mt-4">

View File

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