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(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const navigate = useNavigate(); const queryClient = useQueryClient(); const [openDropdownId, setOpenDropdownId] = useState(null); const [filterStatus, setFilterStatus] = useState("all"); const [issuesByDeliverable, setIssuesByDeliverable] = useState>({}); const currentUser = userSessionService.getSession(); const isAdminRole = currentUser?.roleName && ['CEO', 'COO', 'Admin'].includes(currentUser.roleName); const [dateFilter, setDateFilter] = useState("all"); const [projectFilter, setProjectFilter] = useState("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>((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>((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 (
{/* Header Section */}

Deliverables Dashboard

Manage and track all project deliverables

{currentUser && (

Logged in as {currentUser.firstName} {currentUser.lastName} {currentUser.roleName && ( • Role: {currentUser.roleName} )} {isAdminRole && ( Viewing all deliverables )} {!isAdminRole && ( Viewing deliverables from assigned projects only )}

)}
{/* Statistics Grid */}

Total Deliverables

{totalDeliverables}

Upcoming

{upcomingDeliverables}

{Math.round((upcomingDeliverables / (totalDeliverables || 1)) * 100)}%

Completed

{pastDeliverables}

Completion Rate

{completionRate.toFixed(0)}%

{/* Enhanced Filters */}
{/* Search Input */}
setSearchQuery(e.target.value)} className="w-full pl-8 h-8 text-sm bg-background border-none shadow-sm" />
{/* Status Filter */} {/* Project Filter */} ({ 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 */} ({ 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") && ( )}
{/* View Toggle */}
{/* Deliverables List */} {isLoading ? (
{[1, 2, 3].map((i) => (
))}
) : deliverablesError || projectsError || issuesError ? (

Error loading data

) : filteredDeliverables.length === 0 ? (

No deliverables found

) : viewMode === "card" ? (
{Object.entries(deliverablesByMonth).map(([month, monthDeliverables]) => (

{month}

{monthDeliverables.map((deliverable: Deliverable) => { const deliveryDate = new Date(deliverable.deliveryDate); const issuesCount = getIssuesCount(deliverable.id); const getStatusBadge = () => { if (deliverable.status === "Overdue") { return Overdue; } else if (deliverable.status === "Completed") { return Completed; } else if (deliverable.status === "In Progress") { return In Progress; } else { return {deliverable.status || "Pending"}; } }; return ( navigate(`/deliverable-details/${deliverable.id}`)} >
{deliverable.name} Project #{deliverable.projectId}
{getStatusBadge()}

{deliverable.description || "No description provided"}

{format(deliveryDate, "MMM d, yyyy")}
{issuesCount} {issuesCount === 1 ? 'issue' : 'issues'}
); })}
))}
) : ( Name Project Description Delivery Date Issues Status Actions {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 ( navigate(`/deliverable-details/${deliverable.id}`)} > {deliverable.name} {getProjectName(deliverable.projectId)}
{deliverable.description || "No description provided"}
{format(deliveryDate, "MMM d, yyyy")}
{issuesCount} {deliverable.status === "Overdue" ? ( Overdue ) : deliverable.status === "Completed" ? ( Completed ) : deliverable.status === "In Progress" ? ( In Progress ) : ( {deliverable.status || "Pending"} )} e.stopPropagation()}> { e.stopPropagation(); navigate(`/deliverable-details/${deliverable.id}`); }} > View Details { e.stopPropagation(); handleDeleteDeliverable(deliverable.id); }} className="text-destructive" > Delete
); })}
)} Are you absolutely sure? This action cannot be undone. This will permanently delete the deliverable. Cancel Delete
); }; export default DeliverablesList;