252 lines
8.3 KiB
TypeScript
252 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Trash2,
|
|
Eye,
|
|
AlertTriangle
|
|
} from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
DialogTrigger
|
|
} from "@/components/ui/dialog";
|
|
import { Announcement } from "@/app/types/announcement";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
|
|
interface AnnouncementsListProps {
|
|
userRole: string[];
|
|
}
|
|
|
|
export function AnnouncementsList({ userRole }: AnnouncementsListProps) {
|
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null);
|
|
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(() => {
|
|
fetchAnnouncements();
|
|
}, []);
|
|
|
|
// Handle viewing an announcement
|
|
const handleViewAnnouncement = (announcement: Announcement) => {
|
|
setSelectedAnnouncement(announcement);
|
|
setIsViewDialogOpen(true);
|
|
};
|
|
|
|
// Handle deleting an announcement
|
|
const handleDeleteClick = (announcement: Announcement) => {
|
|
setSelectedAnnouncement(announcement);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
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",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Format roles for display
|
|
const formatRoles = (roles: string[]) => {
|
|
return roles.map(role => {
|
|
const roleName = role === "all"
|
|
? "All Users"
|
|
: role.charAt(0).toUpperCase() + role.slice(1);
|
|
|
|
return (
|
|
<Badge key={role} variant="outline" className="mr-1">
|
|
{roleName}
|
|
</Badge>
|
|
);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<CardTitle>All Announcements</CardTitle>
|
|
<CardDescription>
|
|
Manage announcements for different user roles
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Announcements table with scroll */}
|
|
{loading ? (
|
|
<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>
|
|
) : announcements.length === 0 ? (
|
|
<div className="text-center py-10 text-gray-500">
|
|
No announcements found
|
|
</div>
|
|
) : (
|
|
<div className="border rounded-md max-h-[500px] overflow-y-auto">
|
|
<Table>
|
|
<TableHeader className="sticky top-0 bg-white z-10">
|
|
<TableRow>
|
|
<TableHead>Title</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead>Author</TableHead>
|
|
<TableHead>Target Roles</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{announcements.map((announcement) => (
|
|
<TableRow key={announcement.id}>
|
|
<TableCell className="font-medium">{announcement.title}</TableCell>
|
|
<TableCell>{new Date(announcement.createdAt).toLocaleDateString()}</TableCell>
|
|
<TableCell>{announcement.author.email}</TableCell>
|
|
<TableCell>{formatRoles(announcement.targetRoles)}</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleViewAnnouncement(announcement)}
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
<span className="sr-only">View</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleDeleteClick(announcement)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
<span className="sr-only">Delete</span>
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
{/* View Announcement Dialog */}
|
|
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
|
<DialogContent className="sm:max-w-xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{selectedAnnouncement?.title}</DialogTitle>
|
|
<DialogDescription>
|
|
Posted by {selectedAnnouncement?.author.email} on {selectedAnnouncement && new Date(selectedAnnouncement.createdAt).toLocaleDateString()}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="mt-4">
|
|
<div className="mb-2">
|
|
<span className="text-sm text-gray-500">Target Audience:</span>{" "}
|
|
{selectedAnnouncement && formatRoles(selectedAnnouncement.targetRoles)}
|
|
</div>
|
|
<p className="text-sm leading-6 text-gray-700">
|
|
{selectedAnnouncement?.content}
|
|
</p>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<AlertTriangle className="text-red-500 h-5 w-5" />
|
|
Confirm Deletion
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete the announcement "{selectedAnnouncement?.title}"? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter className="mt-4">
|
|
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="destructive" onClick={confirmDelete}>
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|