Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { Edit, MoreHorizontal, Plus, Search, Trash, Calendar, Check, X, ChevronDown, LayoutGrid, Table2, BarChart3, Clock, CheckCircle2 } from "lucide-react"; | |
| import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; | |
| import { format, parseISO } from "date-fns"; | |
| import { toast } from "@/lib/custom-toast"; | |
| // UI Components | |
| import Layout from "@/components/layout/layout"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { | |
| AlertDialog, | |
| AlertDialogAction, | |
| AlertDialogCancel, | |
| AlertDialogContent, | |
| AlertDialogDescription, | |
| AlertDialogFooter, | |
| AlertDialogHeader, | |
| AlertDialogTitle, | |
| AlertDialogTrigger, | |
| } from "@/components/ui/alert-dialog"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuTrigger, | |
| DropdownMenuSeparator, | |
| DropdownMenuLabel, | |
| DropdownMenuCheckboxItem, | |
| DropdownMenuGroup, | |
| DropdownMenuRadioGroup, | |
| DropdownMenuRadioItem, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { Skeleton } from "@/components/ui/skeleton"; | |
| import { Progress } from "@/components/ui/progress"; | |
| import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; | |
| import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Checkbox } from "@/components/ui/checkbox"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Dropdown, DropdownOption } from "@/components/ui/dropdown"; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow, | |
| } from "@/components/ui/table"; | |
| // Types & API | |
| import { Deliverable, ProjectApi, IssueApi } from "@/types"; | |
| import { deliverablesApi, projectService, issuesApi, userSessionService } from "@/lib/api"; | |
| const DeliverablesList = () => { | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [deliverableToDelete, setDeliverableToDelete] = useState<number | null>(null); | |
| const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | |
| const navigate = useNavigate(); | |
| const queryClient = useQueryClient(); | |
| const [openDropdownId, setOpenDropdownId] = useState<string | null>(null); | |
| const [filterStatus, setFilterStatus] = useState("all"); | |
| const [issuesByDeliverable, setIssuesByDeliverable] = useState<Record<number, IssueApi[]>>({}); | |
| const currentUser = userSessionService.getSession(); | |
| const isAdminRole = currentUser?.roleName && ['CEO', 'COO', 'Admin'].includes(currentUser.roleName); | |
| const [dateFilter, setDateFilter] = useState<string>("all"); | |
| const [projectFilter, setProjectFilter] = useState<string>("all"); | |
| const [viewMode, setViewMode] = useState<"card" | "table">("table"); | |
| // Fetch deliverables | |
| const { | |
| isLoading: isLoadingDeliverables, | |
| error: deliverablesError, | |
| data: deliverables = [] | |
| } = useQuery({ | |
| queryKey: ['deliverables'], | |
| queryFn: deliverablesApi.getAll, | |
| }); | |
| // Fetch projects for dropdown | |
| const { | |
| isLoading: isLoadingProjects, | |
| error: projectsError, | |
| data: projects = [] | |
| } = useQuery({ | |
| queryKey: ['projects'], | |
| queryFn: projectService.getAll, | |
| }); | |
| // Fetch all issues | |
| const { | |
| isLoading: isLoadingIssues, | |
| error: issuesError, | |
| data: allIssues = [] | |
| } = useQuery({ | |
| queryKey: ['issues'], | |
| queryFn: issuesApi.getAll, | |
| }); | |
| // Group issues by deliverable ID | |
| useEffect(() => { | |
| if (allIssues.length > 0) { | |
| const issueGroups = allIssues.reduce<Record<number, IssueApi[]>>((acc, issue) => { | |
| if (issue.deliverablesId) { | |
| if (!acc[issue.deliverablesId]) { | |
| acc[issue.deliverablesId] = []; | |
| } | |
| acc[issue.deliverablesId].push(issue); | |
| } | |
| return acc; | |
| }, {}); | |
| setIssuesByDeliverable(issueGroups); | |
| } | |
| }, [allIssues]); | |
| // Delete mutation | |
| const deleteDeliverableMutation = useMutation({ | |
| mutationFn: (id: number) => deliverablesApi.delete(id), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['deliverables'] }); | |
| toast.success('Deliverable deleted successfully'); | |
| setDeliverableToDelete(null); | |
| setDeleteDialogOpen(false); | |
| }, | |
| onError: (error) => { | |
| console.error('Delete error:', error); | |
| toast.error('Failed to delete deliverable'); | |
| } | |
| }); | |
| // Filter deliverables by search query and selected project | |
| const filteredDeliverables = deliverables.filter((deliverable: Deliverable) => { | |
| const matchesSearch = | |
| deliverable.name.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| deliverable.description.toLowerCase().includes(searchQuery.toLowerCase()); | |
| const matchesProject = | |
| projectFilter === "all" || | |
| (projectFilter !== "all" && deliverable.projectId === parseInt(projectFilter)); | |
| const matchesStatus = | |
| filterStatus === "all" || | |
| (deliverable.status && deliverable.status === filterStatus) || | |
| (!deliverable.status && filterStatus === "Pending"); | |
| const matchesDate = | |
| dateFilter === "all" || | |
| (dateFilter !== "all" && format(new Date(deliverable.deliveryDate), "yyyy-MM") === dateFilter); | |
| return matchesSearch && matchesProject && matchesStatus && matchesDate; | |
| }); | |
| // Handle delete deliverable | |
| const handleDeleteDeliverable = (id: number) => { | |
| setDeliverableToDelete(id); | |
| setDeleteDialogOpen(true); | |
| }; | |
| // Confirm delete | |
| const confirmDelete = () => { | |
| if (deliverableToDelete) { | |
| deleteDeliverableMutation.mutate(deliverableToDelete); | |
| } | |
| }; | |
| // Find project name by ID | |
| const getProjectName = (projectId: number): string => { | |
| const project = projects.find((p: ProjectApi) => p.id === projectId); | |
| return project ? project.projectName : "Unknown Project"; | |
| }; | |
| // Calculate statistics | |
| const totalDeliverables = deliverables.length; | |
| const upcomingDeliverables = deliverables.filter( | |
| (d) => new Date(d.deliveryDate) > new Date() | |
| ).length; | |
| const pastDeliverables = deliverables.filter( | |
| (d) => new Date(d.deliveryDate) <= new Date() | |
| ).length; | |
| const completionRate = Math.round((pastDeliverables / (totalDeliverables || 1)) * 100); | |
| // Group deliverables by month | |
| const deliverablesByMonth = filteredDeliverables.reduce<Record<string, Deliverable[]>>((acc, deliverable) => { | |
| const month = format(parseISO(deliverable.deliveryDate), "MMMM yyyy"); | |
| if (!acc[month]) { | |
| acc[month] = []; | |
| } | |
| acc[month].push(deliverable); | |
| return acc; | |
| }, {}); | |
| const getStatusColor = (date: string): string => { | |
| const today = new Date(); | |
| const deliveryDate = new Date(date); | |
| const diffDays = Math.ceil((deliveryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); | |
| if (diffDays < 0) return "bg-red-500/10 text-red-500"; | |
| if (diffDays <= 7) return "bg-yellow-500/10 text-yellow-500"; | |
| return "bg-green-500/10 text-green-500"; | |
| }; | |
| // Get issues count for a deliverable | |
| const getIssuesCount = (deliverableId: number): number => { | |
| return issuesByDeliverable[deliverableId]?.length || 0; | |
| }; | |
| // Loading state | |
| const isLoading = isLoadingDeliverables || isLoadingProjects || isLoadingIssues; | |
| const clearAllFilters = () => { | |
| setSearchQuery(""); | |
| setFilterStatus("all"); | |
| setDateFilter("all"); | |
| setProjectFilter("all"); | |
| }; | |
| // Get unique months from deliverables | |
| const getAvailableMonths = () => { | |
| const months: { value: string, label: string }[] = []; | |
| deliverables.forEach(deliverable => { | |
| const date = new Date(deliverable.deliveryDate); | |
| const value = format(date, "yyyy-MM"); | |
| const label = format(date, "MMMM yyyy"); | |
| const existingMonth = months.find(m => m.value === value); | |
| if (!existingMonth) { | |
| months.push({ value, label }); | |
| } | |
| }); | |
| return months.sort((a, b) => a.value.localeCompare(b.value)); | |
| }; | |
| const availableMonths = getAvailableMonths(); | |
| const handleProjectChange = (value: string) => { | |
| setProjectFilter(value); | |
| }; | |
| const handleStatusChange = (value: string) => { | |
| setFilterStatus(value); | |
| }; | |
| const handleDateChange = (value: string) => { | |
| setDateFilter(value); | |
| }; | |
| return ( | |
| <Layout> | |
| <div className="container mx-auto p-4 md:p-6 space-y-6"> | |
| {/* Header Section */} | |
| <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> | |
| <div> | |
| <h1 className="text-2xl font-bold tracking-tight">Deliverables Dashboard</h1> | |
| <p className="text-muted-foreground"> | |
| Manage and track all project deliverables | |
| </p> | |
| {currentUser && ( | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| Logged in as <span className="font-medium">{currentUser.firstName} {currentUser.lastName}</span> | |
| {currentUser.roleName && ( | |
| <span> • Role: <span className="font-medium">{currentUser.roleName}</span></span> | |
| )} | |
| {isAdminRole && ( | |
| <span> • <span className="text-green-600">Viewing all deliverables</span></span> | |
| )} | |
| {!isAdminRole && ( | |
| <span> • <span className="text-blue-600">Viewing deliverables from assigned projects only</span></span> | |
| )} | |
| </p> | |
| )} | |
| </div> | |
| <Button | |
| onClick={() => navigate("/new-deliverable")} | |
| className="bg-primary text-white hover:bg-primary/90" | |
| > | |
| Create New Deliverable | |
| </Button> | |
| </div> | |
| {/* Statistics Grid */} | |
| <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> | |
| <Card className="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20"> | |
| <CardContent className="p-3 flex items-center justify-between"> | |
| <div> | |
| <p className="text-[10px] font-semibold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-wider">Total Deliverables</p> | |
| <div className="flex items-baseline gap-2"> | |
| <h3 className="text-xl font-bold">{totalDeliverables}</h3> | |
| </div> | |
| </div> | |
| <div className="rounded-full p-2 bg-blue-500/10"> | |
| <BarChart3 className="h-4 w-4 text-blue-600" /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20"> | |
| <CardContent className="p-3 flex items-center justify-between"> | |
| <div> | |
| <p className="text-[10px] font-semibold text-green-600/70 dark:text-green-400/70 uppercase tracking-wider">Upcoming</p> | |
| <div className="flex items-baseline gap-2"> | |
| <h3 className="text-xl font-bold">{upcomingDeliverables}</h3> | |
| <span className="text-[10px] text-muted-foreground font-medium"> | |
| {Math.round((upcomingDeliverables / (totalDeliverables || 1)) * 100)}% | |
| </span> | |
| </div> | |
| </div> | |
| <div className="rounded-full p-2 bg-green-500/10"> | |
| <Clock className="h-4 w-4 text-green-600" /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20"> | |
| <CardContent className="p-3 flex items-center justify-between"> | |
| <div> | |
| <p className="text-[10px] font-semibold text-purple-600/70 dark:text-purple-400/70 uppercase tracking-wider">Completed</p> | |
| <div className="flex items-baseline gap-2"> | |
| <h3 className="text-xl font-bold">{pastDeliverables}</h3> | |
| </div> | |
| </div> | |
| <div className="rounded-full p-2 bg-purple-500/10"> | |
| <CheckCircle2 className="h-4 w-4 text-purple-600" /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-900/20 dark:to-amber-800/20"> | |
| <CardContent className="p-3 flex items-center justify-between"> | |
| <div> | |
| <p className="text-[10px] font-semibold text-amber-600/70 dark:text-amber-400/70 uppercase tracking-wider">Completion Rate</p> | |
| <h3 className="text-xl font-bold">{completionRate.toFixed(0)}%</h3> | |
| </div> | |
| <div className="rounded-full p-2 bg-amber-500/10"> | |
| <BarChart3 className="h-4 w-4 text-amber-600" /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Enhanced Filters */} | |
| <Card className="border border-border shadow-sm bg-accent/5 backdrop-blur-sm"> | |
| <CardContent className="p-2"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {/* Search Input */} | |
| <div className="relative flex-1 min-w-[200px]"> | |
| <Search className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search deliverables..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="w-full pl-8 h-8 text-sm bg-background border-none shadow-sm" | |
| /> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {/* Status Filter */} | |
| <Dropdown | |
| options={[ | |
| { label: "All Statuses", value: "all" }, | |
| { label: "Pending", value: "Pending" }, | |
| { label: "In Progress", value: "In Progress" }, | |
| { label: "Completed", value: "Completed" }, | |
| { label: "Overdue", value: "Overdue" } | |
| ]} | |
| value={filterStatus} | |
| onValueChange={handleStatusChange} | |
| containerClassName="w-auto min-w-[130px]" | |
| className="h-8 border-border bg-background shadow-sm text-xs" | |
| /> | |
| {/* Project Filter */} | |
| <Dropdown | |
| options={[ | |
| { label: "All Projects", value: "all" }, | |
| ...projects.map((project: ProjectApi) => ({ | |
| label: project.projectName, | |
| value: project.id.toString() | |
| })) | |
| ]} | |
| value={projectFilter} | |
| onValueChange={handleProjectChange} | |
| containerClassName="w-auto min-w-[130px]" | |
| className="h-8 border-border bg-background shadow-sm text-xs" | |
| /> | |
| {/* Date Filter */} | |
| <Dropdown | |
| options={[ | |
| { label: "All Months", value: "all" }, | |
| ...availableMonths.map((month) => ({ | |
| label: month.label, | |
| value: month.value | |
| })) | |
| ]} | |
| value={dateFilter} | |
| onValueChange={handleDateChange} | |
| containerClassName="w-auto min-w-[130px]" | |
| className="h-8 border-border bg-background shadow-sm text-xs" | |
| /> | |
| {/* Clear Filters Button */} | |
| {(searchQuery || filterStatus !== "all" || projectFilter !== "all" || dateFilter !== "all") && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-8 px-2 text-xs" | |
| onClick={clearAllFilters} | |
| > | |
| <X className="h-3 w-3 mr-1" /> | |
| Clear | |
| </Button> | |
| )} | |
| <div className="h-6 w-px bg-border/50 mx-1" /> | |
| {/* View Toggle */} | |
| <div className="flex rounded-md bg-background shadow-sm h-8 p-0.5 border border-border"> | |
| <Button | |
| variant={viewMode === "card" ? "default" : "ghost"} | |
| size="icon" | |
| className={`h-full w-7 rounded-sm ${viewMode === "card" ? "bg-[#ff6600] hover:bg-orange-600 text-white" : ""}`} | |
| onClick={() => setViewMode("card")} | |
| > | |
| <LayoutGrid className="h-3.5 w-3.5" /> | |
| </Button> | |
| <Button | |
| variant={viewMode === "table" ? "default" : "ghost"} | |
| size="icon" | |
| className={`h-full w-7 rounded-sm ${viewMode === "table" ? "bg-[#ff6600] hover:bg-orange-600 text-white" : ""}`} | |
| onClick={() => setViewMode("table")} | |
| > | |
| <Table2 className="h-3.5 w-3.5" /> | |
| </Button> | |
| </div> | |
| <Button | |
| size="sm" | |
| className="h-8 px-3 text-xs" | |
| onClick={() => navigate("/new-deliverable")} | |
| > | |
| <Plus className="h-3.5 w-3.5 mr-1" /> New | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Deliverables List */} | |
| {isLoading ? ( | |
| <div className="space-y-4"> | |
| {[1, 2, 3].map((i) => ( | |
| <Card key={i}> | |
| <CardContent className="p-6"> | |
| <div className="space-y-2"> | |
| <Skeleton className="h-4 w-[250px]" /> | |
| <Skeleton className="h-4 w-[200px]" /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| ) : deliverablesError || projectsError || issuesError ? ( | |
| <div className="flex items-center justify-center py-8"> | |
| <p className="text-red-500">Error loading data</p> | |
| </div> | |
| ) : filteredDeliverables.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center py-8 gap-2"> | |
| <p className="text-muted-foreground">No deliverables found</p> | |
| <Button | |
| variant="outline" | |
| onClick={() => navigate("/new-deliverable")} | |
| className="mt-2" | |
| > | |
| <Plus className="h-4 w-4 mr-2" /> | |
| Add your first deliverable | |
| </Button> | |
| </div> | |
| ) : viewMode === "card" ? ( | |
| <div className="space-y-6"> | |
| {Object.entries(deliverablesByMonth).map(([month, monthDeliverables]) => ( | |
| <div key={month}> | |
| <h2 className="text-lg font-semibold mb-3">{month}</h2> | |
| <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> | |
| {monthDeliverables.map((deliverable: Deliverable) => { | |
| const deliveryDate = new Date(deliverable.deliveryDate); | |
| const issuesCount = getIssuesCount(deliverable.id); | |
| const getStatusBadge = () => { | |
| if (deliverable.status === "Overdue") { | |
| return <Badge variant="destructive">Overdue</Badge>; | |
| } else if (deliverable.status === "Completed") { | |
| return <Badge className="bg-green-500/10 text-green-500">Completed</Badge>; | |
| } else if (deliverable.status === "In Progress") { | |
| return <Badge className="bg-blue-500/10 text-blue-500">In Progress</Badge>; | |
| } else { | |
| return <Badge className="bg-yellow-500/10 text-yellow-500">{deliverable.status || "Pending"}</Badge>; | |
| } | |
| }; | |
| return ( | |
| <Card | |
| key={deliverable.id} | |
| className="hover:shadow-md transition-shadow cursor-pointer" | |
| onClick={() => navigate(`/deliverable-details/${deliverable.id}`)} | |
| > | |
| <CardHeader className="pb-2"> | |
| <div className="flex justify-between items-start"> | |
| <div className="space-y-1"> | |
| <CardTitle className="text-lg line-clamp-1"> | |
| {deliverable.name} | |
| </CardTitle> | |
| <CardDescription> | |
| Project #{deliverable.projectId} | |
| </CardDescription> | |
| </div> | |
| {getStatusBadge()} | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <p className="text-sm text-muted-foreground line-clamp-2"> | |
| {deliverable.description || "No description provided"} | |
| </p> | |
| <div className="mt-4 flex items-center justify-between text-sm"> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| {format(deliveryDate, "MMM d, yyyy")} | |
| </span> | |
| </div> | |
| <Badge variant="secondary"> | |
| {issuesCount} {issuesCount === 1 ? 'issue' : 'issues'} | |
| </Badge> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <Card> | |
| <CardContent className="p-0"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead className="w-[250px]">Name</TableHead> | |
| <TableHead>Project</TableHead> | |
| <TableHead>Description</TableHead> | |
| <TableHead className="w-[130px]">Delivery Date</TableHead> | |
| <TableHead className="w-[100px] text-center">Issues</TableHead> | |
| <TableHead className="w-[120px]">Status</TableHead> | |
| <TableHead className="w-[80px]">Actions</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {filteredDeliverables.map((deliverable: Deliverable) => { | |
| const deliveryDate = new Date(deliverable.deliveryDate); | |
| const isOverdue = deliveryDate < new Date(); | |
| const statusColor = getStatusColor(deliverable.deliveryDate); | |
| const issuesCount = getIssuesCount(deliverable.id); | |
| return ( | |
| <TableRow | |
| key={deliverable.id} | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => navigate(`/deliverable-details/${deliverable.id}`)} | |
| > | |
| <TableCell className="font-medium"> | |
| {deliverable.name} | |
| </TableCell> | |
| <TableCell> | |
| {getProjectName(deliverable.projectId)} | |
| </TableCell> | |
| <TableCell className="max-w-[300px]"> | |
| <div className="truncate"> | |
| {deliverable.description || "No description provided"} | |
| </div> | |
| </TableCell> | |
| <TableCell> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-4 w-4 text-muted-foreground" /> | |
| {format(deliveryDate, "MMM d, yyyy")} | |
| </div> | |
| </TableCell> | |
| <TableCell className="text-center"> | |
| <Badge variant="secondary"> | |
| {issuesCount} | |
| </Badge> | |
| </TableCell> | |
| <TableCell> | |
| {deliverable.status === "Overdue" ? ( | |
| <Badge variant="destructive" className="text-xs"> | |
| Overdue | |
| </Badge> | |
| ) : deliverable.status === "Completed" ? ( | |
| <Badge className="bg-green-500/10 text-green-500"> | |
| Completed | |
| </Badge> | |
| ) : deliverable.status === "In Progress" ? ( | |
| <Badge className="bg-blue-500/10 text-blue-500"> | |
| In Progress | |
| </Badge> | |
| ) : ( | |
| <Badge className="bg-yellow-500/10 text-yellow-500"> | |
| {deliverable.status || "Pending"} | |
| </Badge> | |
| )} | |
| </TableCell> | |
| <TableCell> | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}> | |
| <Button variant="ghost" size="sm" className="h-8 w-8 p-0"> | |
| <MoreHorizontal className="h-4 w-4" /> | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end"> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| navigate(`/deliverable-details/${deliverable.id}`); | |
| }} | |
| > | |
| <Edit className="h-4 w-4 mr-2" /> | |
| View Details | |
| </DropdownMenuItem> | |
| <DropdownMenuSeparator /> | |
| <DropdownMenuItem | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDeleteDeliverable(deliverable.id); | |
| }} | |
| className="text-destructive" | |
| > | |
| <Trash className="h-4 w-4 mr-2" /> | |
| Delete | |
| </DropdownMenuItem> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| </TableCell> | |
| </TableRow> | |
| ); | |
| })} | |
| </TableBody> | |
| </Table> | |
| </CardContent> | |
| </Card> | |
| )} | |
| <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> | |
| <AlertDialogContent> | |
| <AlertDialogHeader> | |
| <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> | |
| <AlertDialogDescription> | |
| This action cannot be undone. This will permanently delete the deliverable. | |
| </AlertDialogDescription> | |
| </AlertDialogHeader> | |
| <AlertDialogFooter> | |
| <AlertDialogCancel>Cancel</AlertDialogCancel> | |
| <AlertDialogAction | |
| onClick={confirmDelete} | |
| className="bg-destructive hover:bg-destructive/90" | |
| > | |
| Delete | |
| </AlertDialogAction> | |
| </AlertDialogFooter> | |
| </AlertDialogContent> | |
| </AlertDialog> | |
| </div> | |
| </Layout> | |
| ); | |
| }; | |
| export default DeliverablesList; |