observatory
This commit is contained in:
parent
e9142b28de
commit
3e3653183a
100
app/api/announcements/[id]/route.ts
Normal file
100
app/api/announcements/[id]/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/api/announcements/route.ts
Normal file
90
app/api/announcements/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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])
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user