import React, { useState, useEffect, createContext, useContext, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { format, isSameDay } from "date-fns"; import { AlertCircle, CheckCircle, ChevronDown, Clock, Filter, LayoutDashboard, LayoutList, MoreHorizontal, Plus, RefreshCw, Search, Trash2, Eye, Edit2, Bug, Check, User, ShieldAlert, FlameIcon, X, Timer, CalendarIcon, ExternalLink, ChevronRight, ListFilter, Download, FileSpreadsheet, FileText } from "lucide-react"; import * as XLSX from 'xlsx'; import { jsPDF } from 'jspdf'; import { saveAs } from 'file-saver'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import Layout from "@/components/layout/layout"; // Import Task and tasksApi from tasksApi import { Task, tasksApi } from "@/services/tasksApi"; import { employeeService, projectService, teamMemberService } from "@/lib/api"; import { Employee, ProjectApi } from "@/types"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import DashboardBugs from "@/components/dashboard/dashboard-bugs"; import { Dropdown, DropdownOption } from "@/components/ui/dropdown"; import { useAuth } from "@/lib/auth-context"; import { timeLogApi, TimeLogCreateRequest } from "@/services/timeLogApi"; import ImportTasksDialog from "@/components/tasks/import-tasks-dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Calendar, } from "@/components/ui/calendar"; import { Textarea } from "@/components/ui/textarea"; import CustomModal from "@/components/custom-modal"; // Import the custom modal import { exportToExcel } from "@/lib/excel-export"; import autoTable from 'jspdf-autotable'; // Create a dropdown context to manage all dropdowns globally const DropdownContext = createContext<{ openDropdown: string | null; setOpenDropdown: (id: string | null) => void; }>({ openDropdown: null, setOpenDropdown: () => { }, }); // Create a dropdown provider component function DropdownProvider({ children }: { children: React.ReactNode }) { const [openDropdown, setOpenDropdown] = useState(null); return ( {openDropdown &&
setOpenDropdown(null)} />} {children} ); } // Custom Action Menu component for table actions interface CustomActionMenuProps { align?: "left" | "right"; bug: Task; onView: (bug: Task) => void; onEdit: (bug: Task) => void; onDelete: (id: number) => void; isDeleting: boolean; deleteId: number | null; } function CustomActionMenu({ align = "right", bug, onView, onEdit, onDelete, isDeleting, deleteId }: CustomActionMenuProps) { const [open, setOpen] = useState(false); const menuRef = React.useRef(null); const [menuPosition, setMenuPosition] = useState({ top: 0, right: undefined as number | undefined, left: undefined as number | undefined }); const updateMenuPosition = () => { if (menuRef.current) { const rect = menuRef.current.getBoundingClientRect(); setMenuPosition({ top: rect.bottom + window.scrollY, right: align === "right" ? window.innerWidth - rect.right : undefined, left: align === "left" ? rect.left : undefined }); } }; const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); if (!open) { // Update position when opening setTimeout(updateMenuPosition, 0); } }; const handleAction = (action: () => void, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); action(); setOpen(false); }; // Update position when open state changes useEffect(() => { if (open) { updateMenuPosition(); } }, [open]); // Update position on scroll and resize useEffect(() => { if (!open) return; const handleScroll = () => { updateMenuPosition(); }; const handleResize = () => { updateMenuPosition(); }; window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('resize', handleResize); }; }, [open]); // Close dropdown when clicking outside useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [open]); return (
{open && (
handleAction(() => onView(bug), e)} > View Details
handleAction(() => onEdit(bug), e)} > Edit
{ if (!(isDeleting && deleteId === bug.id)) { handleAction(() => onDelete(bug.id), e); } else { e.preventDefault(); e.stopPropagation(); } }} > {isDeleting && deleteId === bug.id ? ( <>
Deleting... ) : ( <> Delete )}
)}
); } // Improved CustomDropdown component with dropdown context interface CustomDropdownProps { id?: string; // Optional identifier for this dropdown value: string; onChange: (value: string) => void; options: { value: string; label: string }[]; placeholder: string; } function CustomDropdown({ id = "dropdown-" + Math.random().toString(36).substring(2, 9), value, onChange, options, placeholder }: CustomDropdownProps) { const { openDropdown, setOpenDropdown } = useContext(DropdownContext); const isOpen = openDropdown === id; const dropdownRef = React.useRef(null); const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 }); const getLabel = () => { return options.find(option => option.value === value)?.label || placeholder; }; const updateMenuPosition = () => { if (dropdownRef.current) { const rect = dropdownRef.current.getBoundingClientRect(); // Calculate available space below and above const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; // Determine if dropdown should open upward or downward const openUpward = spaceBelow < 300 && spaceAbove > spaceBelow; setMenuPosition({ top: openUpward ? rect.top - 5 : rect.bottom + 5, left: rect.left, width: rect.width }); } }; const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // If already open, close it if (isOpen) { setOpenDropdown(null); return; } // Otherwise, update position and open this dropdown // Close any other open dropdown setTimeout(() => { updateMenuPosition(); setOpenDropdown(id); }, 0); }; const handleOptionClick = (optionValue: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Close dropdown first setOpenDropdown(null); // Then change the value setTimeout(() => { onChange(optionValue); }, 0); }; // Update position when open state changes useEffect(() => { if (isOpen) { updateMenuPosition(); } }, [isOpen]); // Update position on scroll and resize when open useEffect(() => { if (!isOpen) return; const handleScroll = () => { updateMenuPosition(); }; const handleResize = () => { updateMenuPosition(); }; window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('resize', handleResize); }; }, [isOpen]); // Close dropdown when clicking outside useEffect(() => { if (!isOpen) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpenDropdown(null); } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [isOpen, setOpenDropdown]); return (
{getLabel()}
{isOpen && (
{options.map((option) => (
handleOptionClick(option.value, e)} > {option.label} {value === option.value && }
))}
)}
); } // Improved CustomEmployeeDropdown component with dropdown context interface CustomEmployeeDropdownProps { id?: string; // Optional identifier for this dropdown value: string | number; onChange: (value: string | number) => void; employees: Employee[]; placeholder: string; } function CustomEmployeeDropdown({ id = "employee-dropdown-" + Math.random().toString(36).substring(2, 9), value, onChange, employees, placeholder }: CustomEmployeeDropdownProps) { const { openDropdown, setOpenDropdown } = useContext(DropdownContext); const isOpen = openDropdown === id; const dropdownRef = React.useRef(null); const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 }); const getEmployeeName = (id: string | number | null) => { if (id === null || id === "all" || id === "unassigned") return placeholder; if (typeof id === "string" && id !== "all") { id = parseInt(id, 10); } const employee = employees.find(emp => emp.id === id); return employee ? `${employee.firstName} ${employee.lastName}` : `Employee ${id}`; }; const getInitials = (id: number) => { const employee = employees.find(emp => emp.id === id); if (!employee) return "??"; return (employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase(); }; const updateMenuPosition = () => { if (dropdownRef.current) { const rect = dropdownRef.current.getBoundingClientRect(); // Calculate available space below and above const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; // Determine if dropdown should open upward or downward const openUpward = spaceBelow < 300 && spaceAbove > spaceBelow; setMenuPosition({ top: openUpward ? rect.top - 5 : rect.bottom + 5, left: rect.left, width: rect.width }); } }; const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // If already open, close it if (isOpen) { setOpenDropdown(null); return; } // Otherwise, update position and open this dropdown setTimeout(() => { updateMenuPosition(); setOpenDropdown(id); }, 0); }; const handleOptionClick = (employeeId: string | number, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); // Close menu first setOpenDropdown(null); // Then change the value with a small delay setTimeout(() => { onChange(employeeId); }, 0); }; // Update position when open state changes useEffect(() => { if (isOpen) { updateMenuPosition(); } }, [isOpen]); // Update position on scroll and resize useEffect(() => { if (!isOpen) return; const handleScroll = () => { updateMenuPosition(); }; const handleResize = () => { updateMenuPosition(); }; window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('resize', handleResize); }; }, [isOpen]); // Close dropdown when clicking outside useEffect(() => { if (!isOpen) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpenDropdown(null); } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [isOpen, setOpenDropdown]); return (
{value !== "all" && typeof value === "number" ? ( {getInitials(value)} ) : ( )} {getEmployeeName(value)}
{isOpen && (
handleOptionClick("all", e)} > All Assignees {value === "all" && ( )}
{employees.map((employee) => (
handleOptionClick(employee.id, e)} > {(employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase()} {employee.firstName} {employee.lastName} {value === employee.id && ( )}
))}
)}
); } // The Task interface in tasksApi.ts now includes all required fields const Bugs = () => { const navigate = useNavigate(); const { userData } = useAuth(); // Add useAuth to get current user data const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [severityFilter, setSeverityFilter] = useState("all"); const [assigneeFilter, setAssigneeFilter] = useState("all"); const [projectFilter, setProjectFilter] = useState("all"); // Add new filter for My/Team bugs const [scopeFilter, setScopeFilter] = useState<"my" | "all">("my"); const [isLoading, setIsLoading] = useState(true); const [bugs, setBugs] = useState([]); const [employees, setEmployees] = useState([]); const [teamMembers, setTeamMembers] = useState([]); // Add state for team members const [projects, setProjects] = useState>({}); const [userProjects, setUserProjects] = useState([]); // Add state for user's projects const [isDeleting, setIsDeleting] = useState(false); const [deleteId, setDeleteId] = useState(null); const [openMenuId, setOpenMenuId] = useState(null); const [error, setError] = useState(null); const [sortField, setSortField] = useState("createdAt"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); // Newest first by default const [viewMode, setViewMode] = useState<"dashboard" | "list">("list"); // Add state for filter card expansion const [filtersExpanded, setFiltersExpanded] = useState(false); // Quick View Dialog state const [isQuickViewOpen, setIsQuickViewOpen] = useState(false); const [selectedBugForQuickView, setSelectedBugForQuickView] = useState(null); // Time logging state const [isLoggingTime, setIsLoggingTime] = useState(false); const [selectedBugForTimeLog, setSelectedBugForTimeLog] = useState(null); const [timeLogHours, setTimeLogHours] = useState(""); const [remainingHours, setRemainingHours] = useState(""); const [timeLogDescription, setTimeLogDescription] = useState(""); const [startTime, setStartTime] = useState(""); const [endTime, setEndTime] = useState(""); const [workDate, setWorkDate] = useState(new Date()); const [timeOptions, setTimeOptions] = useState>([]); // Status update state const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [updateStatusId, setUpdateStatusId] = useState(null); // Add new state for collapsing completed bugs const [collapseCompletedBugs, setCollapseCompletedBugs] = useState(true); const statusOptions = ["Open", "In Progress", "In Review", "Ready for QA", "Resolved", "Completed", "Closed", "Reopened"]; const severityOptions = ["High", "Medium", "Low"]; // Create API data cache to prevent multiple calls const apiCache = useRef<{ teamMembers: any[] | null; allTeamMembers: any[] | null; allEmployees: Employee[] | null; }>({ teamMembers: null, allTeamMembers: null, allEmployees: null }); // We don't need this cache anymore // const projectTeamsCache = React.useRef>({}); // Time logging functions const generateTimeOptions = () => { const options: Array<{ label: string, value: string }> = []; // Generate times from 12:00 AM to 11:45 PM in 15-minute increments for (let hour = 0; hour < 24; hour++) { for (let minute = 0; minute < 60; minute += 15) { // Format the time as AM/PM const period = hour < 12 ? 'AM' : 'PM'; const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; const timeString = `${displayHour}:${minute.toString().padStart(2, '0')} ${period}`; options.push({ label: timeString, value: timeString }); } } setTimeOptions(options); }; useEffect(() => { const loadData = async () => { try { if (!userData) { toast.error("User data not found. Please log in again."); return; } setIsLoading(true); console.log("DEBUG: Starting data loading process"); // Load everything sequentially to prevent duplicate calls // 1. First get the employee data for the entire app const employeeData = await employeeService.getAll(); console.log(`DEBUG: Loaded ${employeeData.length} employees`); setEmployees(employeeData); // 2. Get user's projects const projectsData = await projectService.getByEmployeeId(userData.employeeId); console.log(`DEBUG: Loaded ${projectsData.length} projects for user ${userData.employeeId}`); setUserProjects(projectsData); const projectsMap = projectsData.reduce((acc: Record, project: ProjectApi) => { acc[project.id] = project.projectName; return acc; }, {}); setProjects(projectsMap); console.log("DEBUG: Created projects map:", projectsMap); // 3. Get team members (only if we have projects and need to) if (projectsData.length > 0 && teamMembers.length === 0) { // Get all team members const allTeamMembers = await teamMemberService.getAll(); console.log(`DEBUG: Loaded ${allTeamMembers.length} team members`); // We don't need to fetch project teams anymore since we're using all employees console.log("DEBUG: Using all employees for assignee dropdown"); // Set team members to be the same as all employees setTeamMembers(employeeData); } // 4. Finally, get the bugs console.log("DEBUG: Fetching bugs with tasksApi.getByType('Bug')"); const bugTasks = await tasksApi.getByType('Bug'); console.log(`DEBUG: Loaded ${bugTasks.length} bug tasks from API`); if (bugTasks.length === 0) { console.warn("DEBUG: No bugs returned from API"); } else { console.log("DEBUG: First bug from API:", bugTasks[0]); } // Map tasks to include projectId const bugsWithProjectId = bugTasks.map(task => { // Using issuesId as projectId, but handle the case where it might be null return { ...task, projectId: task.issuesId || null } as Task; }); // Filter bugs by project only if projectFilter is applied, otherwise show all bugs let filteredBugs = bugsWithProjectId; // Only filter by project if there are projects and we're not in "all projects" mode if (projectsData.length > 0 && projectFilter !== "all") { filteredBugs = bugsWithProjectId.filter(bug => bug.projectName === projectFilter ); console.log(`DEBUG: Filtered to ${filteredBugs.length} bugs for selected project ${projectFilter}`); console.log(`DEBUG: Project filter value: ${projectFilter}`); console.log(`DEBUG: Sample bug projectName: ${filteredBugs.length > 0 ? filteredBugs[0].projectName : 'none'}`); } console.log(`DEBUG: Final bugs count: ${filteredBugs.length} bugs`); if (filteredBugs.length === 0) { console.warn("DEBUG: No bugs left after filtering"); if (bugTasks.length > 0) { console.log("DEBUG: Example bug before filtering:", bugTasks[0]); console.log("DEBUG: Project IDs in bugs:", bugsWithProjectId.map(b => b.projectId)); console.log("DEBUG: User projects:", projectsData.map(p => p.id)); } } else { console.log("DEBUG: Example bug after filtering:", filteredBugs[0]); } // Set the bugs state with our filtered bugs setBugs(filteredBugs); } catch (error) { console.error("Error loading data:", error); toast.error("Error loading data. Please try again."); } finally { setIsLoading(false); } }; // Only load data once when user data is available loadData(); // Also initialize time options generateTimeOptions(); }, [userData]); // Only run when userData changes const getYesterday = () => { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); return yesterday; }; const calculateHoursSpent = (start: string, end: string): string => { // Extract hours and minutes from time strings like "9:30 AM" or "2:45 PM" const parseTime = (timeStr: string) => { const [timePart, periodPart] = timeStr.split(' '); const [hourStr, minuteStr] = timePart.split(':'); let hour = parseInt(hourStr, 10); const minute = parseInt(minuteStr, 10); // Adjust hour based on AM/PM if (periodPart === 'PM' && hour < 12) { hour += 12; } else if (periodPart === 'AM' && hour === 12) { hour = 0; } return { hour, minute }; }; const startTime = parseTime(start); const endTime = parseTime(end); // Calculate total minutes let startMinutes = startTime.hour * 60 + startTime.minute; let endMinutes = endTime.hour * 60 + endTime.minute; // If end time is earlier than start time, assume it's the next day if (endMinutes < startMinutes) { endMinutes += 24 * 60; // Add 24 hours in minutes } // Calculate difference in hours const hoursDiff = (endMinutes - startMinutes) / 60; // Round to nearest quarter hour (0.25) const roundedHours = Math.round(hoursDiff * 4) / 4; return roundedHours.toFixed(2); }; const convertTo24HourFormat = (timeStr: string): string => { const [timePart, ampm] = timeStr.split(' '); let [hourStr, minuteStr] = timePart.split(':'); let hour = parseInt(hourStr, 10); const minute = parseInt(minuteStr, 10); if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; }; const handleTimeLogSubmit = async (bugId: number) => { if (!timeLogHours || isNaN(Number(timeLogHours)) || Number(timeLogHours) <= 0) { toast.error("Please enter valid hours"); return; } if (!startTime || !endTime) { toast.error("Please enter both start and end time"); return; } if (!workDate) { toast.error("Please select a work date"); return; } try { // Convert AM/PM time to 24-hour format for API const startTime24 = convertTo24HourFormat(startTime); const endTime24 = convertTo24HourFormat(endTime); const timeLogData: TimeLogCreateRequest = { workDate: workDate.toISOString(), startTime: startTime24, // Using 24-hour format endTime: endTime24, // Using 24-hour format comments: timeLogDescription || "", createdBy: String(userData.userId), updatedBy: String(userData.userId), remainingHr: parseFloat(remainingHours) || 0, taskId: bugId, hrsSpent: Number(timeLogHours), }; await timeLogApi.create(timeLogData); toast.success("Time logged successfully"); setIsLoggingTime(false); setSelectedBugForTimeLog(null); setTimeLogHours(""); setTimeLogDescription(""); setStartTime(""); setEndTime(""); setWorkDate(new Date()); } catch (error) { console.error("Error logging time:", error); toast.error("Failed to log time"); } }; const getEmployeeName = (employeeId: number): string => { const employee = employees.find(emp => emp.id === employeeId); return employee ? `${employee.firstName} ${employee.lastName}` : `User ${employeeId}`; }; const getEmployeeInitials = (employeeId: number): string => { const employee = employees.find(emp => emp.id === employeeId); return employee ? `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase() : "UN"; }; const sortBugs = (bugsToSort: Task[]): Task[] => { return [...bugsToSort].sort((a, b) => { let valueA, valueB; // Extract the values to compare based on the sort field switch (sortField) { case "title": valueA = a.title?.toLowerCase() || ""; valueB = b.title?.toLowerCase() || ""; break; case "status": valueA = a.status?.toLowerCase() || ""; valueB = b.status?.toLowerCase() || ""; break; case "assignedTo": valueA = getEmployeeName(a.assignedTo || 0).toLowerCase(); valueB = getEmployeeName(b.assignedTo || 0).toLowerCase(); break; case "severity": valueA = a.type?.toLowerCase() || ""; valueB = b.type?.toLowerCase() || ""; break; case "createdAt": default: // For dates, we need to convert to actual Date objects valueA = a.createdAt ? new Date(a.createdAt).getTime() : 0; valueB = b.createdAt ? new Date(b.createdAt).getTime() : 0; break; } // Compare based on sort order if (sortOrder === "asc") { return valueA > valueB ? 1 : valueA < valueB ? -1 : 0; } else { return valueA < valueB ? 1 : valueA > valueB ? -1 : 0; } }); }; // Helper function to get severity value from a bug const getBugSeverity = (bug: Task): string => { // First check if we have a severity field if (bug.severity) { return bug.severity; } // Fall back to type field which may have been used for severity return bug.type || ''; }; // Add this function to determine if a bug should be hidden const shouldShowBug = (bug: Task) => { const completedStatuses = ["Completed", "Closed", "Resolved"]; if (collapseCompletedBugs && completedStatuses.includes(bug.status)) { return false; } return true; }; // Add this function to determine if a bug should be shown based on the scope filter const isInSelectedScope = (bug: Task) => { if (scopeFilter === "all") return true; if (scopeFilter === "my" && bug.assignedTo === userData?.employeeId) return true; return false; }; // Modify the filtered and sorted bugs function to handle collapsing const filteredAndSortedBugs = () => { console.log("DEBUG: Running filteredAndSortedBugs function"); console.log(`DEBUG: Current bugs array length: ${bugs.length}`); let filtered = [...bugs]; // Log current filters console.log("DEBUG: Applied filters:", { searchQuery, statusFilter, severityFilter, assigneeFilter, projectFilter, scopeFilter }); // Apply scope filter (My/Team/All bugs) if (scopeFilter !== "all") { filtered = filtered.filter(isInSelectedScope); console.log(`DEBUG: After scope filter: ${filtered.length} bugs`); } // Apply text search filter if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(bug => bug.title?.toLowerCase().includes(query) || bug.description?.toLowerCase().includes(query) || bug.taskCode?.toLowerCase().includes(query) || bug.version?.toLowerCase().includes(query) ); console.log(`DEBUG: After search filter: ${filtered.length} bugs`); } // Apply status filter if (statusFilter !== "all") { filtered = filtered.filter(bug => bug.status === statusFilter); console.log(`DEBUG: After status filter: ${filtered.length} bugs`); } // Apply severity filter (check both severity and type fields) if (severityFilter !== "all") { filtered = filtered.filter(bug => getBugSeverity(bug) === severityFilter || bug.type === severityFilter ); console.log(`DEBUG: After severity filter: ${filtered.length} bugs`); } // Apply assignee filter if (assigneeFilter !== "all") { filtered = filtered.filter(bug => bug.assignedTo?.toString() === assigneeFilter); console.log(`DEBUG: After assignee filter: ${filtered.length} bugs`); } // Apply project filter if (projectFilter !== "all") { filtered = filtered.filter(bug => bug.projectName === projectFilter ); console.log(`DEBUG: After project filter: ${filtered.length} bugs`); console.log(`DEBUG: Project filter value: ${projectFilter}`); console.log(`DEBUG: Sample bug projectName: ${filtered.length > 0 ? filtered[0].projectName : 'none'}`); } console.log(`DEBUG: Final filtered bugs count: ${filtered.length}`); // Apply sorting const sorted = sortBugs(filtered); return sorted; }; // Count completed bugs that are currently filtered const getCompletedBugsCount = () => { const completedStatuses = ["Completed", "Closed", "Resolved"]; return filteredAndSortedBugs().filter(bug => completedStatuses.includes(bug.status) ).length; }; const handleCreate = () => { navigate("/bugs/new"); }; const handleEdit = (bug: Task) => { navigate(`/bugs/${bug.id}/edit`); }; const handleViewDetails = (bug: Task) => { navigate(`/bugs/${bug.id}`); }; const handleDelete = async (id: number) => { try { setIsDeleting(true); setDeleteId(id); await tasksApi.delete(id); toast.success("Bug deleted successfully"); // Refresh the bug list const bugTasks = await tasksApi.getByType('Bug'); // Map tasks to include projectId const bugsWithProjectId = bugTasks.map(task => { return { ...task, projectId: task.issuesId } as Task; }) .filter(bug => { // If no user projects are loaded yet, don't filter if (userProjects.length === 0) return true; // Keep bugs that belong to the user's projects return bug.projectName && userProjects.some(p => p.projectName === bug.projectName); }); setBugs(bugsWithProjectId); } catch (error) { console.error("Error deleting bug:", error); toast.error("Failed to delete bug"); } finally { setIsDeleting(false); setDeleteId(null); } }; const getStatusBadge = (status: string) => { switch (status) { case "Completed": case "Closed": case "Resolved": return ( {status} ); case "In Progress": return ( {status} ); case "Re-Open": case "Reopened": return ( {status} ); case "In Review": case "Ready for QA": return ( {status} ); case "Open": case "New": return ( {status || "New"} ); default: return ( {status || "Unknown"} ); } }; const getSeverityBadge = (severity: string) => { // Use our helper function if we're passed a bug object if (typeof severity === 'object') { severity = getBugSeverity(severity); } switch (severity) { case "Blocker": return ( {severity} ); case "Critical": return ( {severity} ); case "Major": case "High": return ( {severity} ); case "Minor": case "Medium": return ( {severity} ); case "Trivial": case "Low": return ( {severity} ); default: return ( {severity || "Unknown"} ); } }; const handleSortChange = (field: string) => { if (field === sortField) { // If clicking the same field, toggle the sort order setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { // If clicking a different field, set it as the new sort field and default to ascending setSortField(field); setSortOrder("asc"); } }; const renderSortIcon = (field: string) => { if (field !== sortField) return null; return ( {sortOrder === "asc" ? "↑" : "↓"} ); }; // Add useEffect to track filter changes useEffect(() => { console.log("Filters changed:", { statusFilter, severityFilter, assigneeFilter, projectFilter }); }, [statusFilter, severityFilter, assigneeFilter, projectFilter]); // Update filter handlers to ensure state update triggers re-render const handleStatusFilterChange = (value: string) => { console.log("Status filter changing to:", value); setStatusFilter(value); }; const handleSeverityFilterChange = (value: string) => { console.log("Severity filter changing to:", value); setSeverityFilter(value); }; const handleAssigneeFilterChange = (value: string | number) => { console.log("Assignee filter changing to:", value); setAssigneeFilter(value === "all" ? "all" : value.toString()); }; const handleProjectFilterChange = (value: string) => { console.log(`Project filter changed to: ${value}`); setProjectFilter(value); console.log(`Setting project filter to: ${value}`); }; // Add new function to handle quick status change const handleStatusChange = async (bug: Task, newStatus: string) => { if (isUpdatingStatus) return; try { setIsUpdatingStatus(true); setUpdateStatusId(bug.id); // Create updated bug object with new status const updatedBug: Task = { ...bug, status: newStatus }; // Call API to update the bug await tasksApi.update(bug.id, updatedBug); // Update local state setBugs(prevBugs => prevBugs.map(b => b.id === bug.id ? { ...b, status: newStatus } : b ) ); toast.success(`Bug status updated to ${newStatus}`); } catch (error) { console.error("Error updating bug status:", error); toast.error("Failed to update status"); } finally { setIsUpdatingStatus(false); setUpdateStatusId(null); } }; // We don't need a separate fetchBugs function anymore as it's handled in the loadData function // But we need a refresh function for the refresh button const handleRefreshBugs = async () => { try { setIsLoading(true); // Only refresh the bugs without reloading team data const bugTasks = await tasksApi.getByType('Bug'); console.log(`DEBUG: Refreshed ${bugTasks.length} bug tasks from API`); // Map tasks to include projectId const bugsWithProjectId = bugTasks.map(task => { return { ...task, projectId: task.issuesId || null } as Task; }); // Don't filter by user projects, show all bugs console.log(`DEBUG: Mapped ${bugsWithProjectId.length} bugs with project IDs`); setBugs(bugsWithProjectId); toast.success("Bug list refreshed"); } catch (error) { console.error("Error refreshing bugs:", error); toast.error("Failed to refresh bug list"); } finally { setIsLoading(false); } }; const handleQuickView = (bug: Task, e?: React.MouseEvent) => { if (e) { e.preventDefault(); e.stopPropagation(); } setSelectedBugForQuickView(bug); setIsQuickViewOpen(true); }; // Export functionality const handleExportToExcel = () => { try { console.log("handleExportToExcel function called"); // Get filtered bugs data const filteredBugs = filteredAndSortedBugs(); console.log(`Found ${filteredBugs.length} bugs to export to Excel`); if (filteredBugs.length === 0) { toast.error("No bugs to export"); return; } // Format data for Excel - include all available columns const excelData = filteredBugs.map(bug => { // Cast bug to Task for accessing specific properties const bugTask = bug as Task; return { 'Bug ID': bug.taskCode || `BUG-${bug.id}`, 'Title': bug.title || '', 'Status': bug.status || '', 'Severity': getBugSeverity(bug) || '', 'Priority': bug.priority || '', 'Assignee': bug.assignedTo ? getEmployeeName(bug.assignedTo) : 'Unassigned', 'Created By': bugTask.createdByName || 'N/A', 'Project': bugTask.projectName || (bugTask.projectId ? projects[bugTask.projectId as number] || `Project ID: ${bugTask.projectId}` : 'N/A'), 'Issue': bugTask.issueName || 'N/A', 'Deliverable': bugTask.deliverableName || 'N/A', 'Description': bug.description || '', 'Steps to Reproduce': bugTask.stepsToReproduce || '', 'Expected Behavior': bugTask.expectedBehavior || '', 'Actual Behavior': bugTask.actualBehavior || '', 'Related URL': bugTask.relatedUrl || '', 'Version': bug.version || '', 'Created Date': bug.createdAt ? format(new Date(bug.createdAt), "MM/dd/yyyy") : 'N/A' }; }); console.log("Creating Excel workbook directly"); // Create workbook directly const worksheet = XLSX.utils.json_to_sheet(excelData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Bugs"); // Set column widths const colWidths = [ { wch: 15 }, // Bug ID { wch: 50 }, // Title { wch: 15 }, // Status { wch: 15 }, // Severity { wch: 15 }, // Priority { wch: 25 }, // Assignee { wch: 25 }, // Created By { wch: 25 }, // Project { wch: 25 }, // Issue { wch: 25 }, // Deliverable { wch: 80 }, // Description { wch: 80 }, // Steps to Reproduce { wch: 50 }, // Expected Behavior { wch: 50 }, // Actual Behavior { wch: 40 }, // Related URL { wch: 15 }, // Version { wch: 15 } // Created Date ]; worksheet['!cols'] = colWidths; // Generate Excel file directly const filename = `bug_list_${format(new Date(), 'yyyy-MM-dd')}.xlsx`; console.log(`Generating Excel file: ${filename}`); // Use writeFile to directly download the file XLSX.writeFile(workbook, filename); console.log("Excel export completed successfully"); toast.success("Bugs exported to Excel successfully"); } catch (error) { console.error('Error exporting to Excel:', error); toast.error('Failed to export bugs to Excel'); } }; const handleExportToPDF = () => { try { console.log("handleExportToPDF function called"); // Get filtered bugs data const filteredBugs = filteredAndSortedBugs(); console.log(`Found ${filteredBugs.length} bugs to export to PDF`); if (filteredBugs.length === 0) { toast.error("No bugs to export"); return; } // Create a new PDF document console.log("Creating new jsPDF document"); const doc = new jsPDF('landscape'); // Add title doc.setFontSize(18); doc.text('Bug Report', 14, 22); // Add generated date doc.setFontSize(10); doc.text(`Generated on: ${format(new Date(), 'MM/dd/yyyy HH:mm')}`, 14, 30); // Filter and status info const filterInfo = []; if (statusFilter !== 'all') filterInfo.push(`Status: ${statusFilter}`); if (severityFilter !== 'all') filterInfo.push(`Severity: ${severityFilter}`); if (assigneeFilter !== 'all') filterInfo.push(`Assignee: ${employees.find(emp => emp.id.toString() === assigneeFilter)?.firstName || assigneeFilter}`); if (projectFilter !== 'all') filterInfo.push(`Project: ${projectFilter}`); if (filterInfo.length > 0) { doc.text(`Filters: ${filterInfo.join(', ')}`, 14, 36); } // Define table columns - add new fields const tableColumn = ["Bug ID", "Title", "Status", "Severity", "Assignee", "Created By", "Project", "Created Date"]; // Create data for the table - simplified for better PDF display const tableRows = filteredBugs.map(bug => { const bugTask = bug as Task; return [ bug.taskCode || `BUG-${bug.id}`, bug.title || '', // Full title without trimming bug.status || '', getBugSeverity(bug) || '', bug.assignedTo ? getEmployeeName(bug.assignedTo) : 'Unassigned', bugTask.createdByName || 'N/A', bugTask.projectName || (bugTask.projectId ? projects[bugTask.projectId as number] || `Project ID: ${bugTask.projectId}` : 'N/A'), bug.createdAt ? format(new Date(bug.createdAt), "MM/dd/yyyy") : 'N/A', ]; }); // Check if autoTable is available if (typeof autoTable !== 'function') { console.error("autoTable is not properly loaded - check the jspdf-autotable import"); throw new Error("autoTable is not properly loaded - check the jspdf-autotable import"); } // Get page dimensions const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); // Define minimal margins for maximum usable width const margin = 10; console.log("Adding table to PDF document"); try { // Use the autoTable function directly autoTable(doc, { head: [tableColumn], body: tableRows, startY: 45, theme: 'grid', styles: { fontSize: 8, cellPadding: 2, overflow: 'linebreak', // This setting will wrap long text lineWidth: 0.1, halign: 'left', // Horizontal alignment }, // Set column widths to undefined to allow autoTable to calculate // optimal widths for filling the whole page width columnStyles: {}, headStyles: { fillColor: [66, 66, 124], textColor: 255, fontStyle: 'bold' }, alternateRowStyles: { fillColor: [240, 240, 240] }, // Set margins to use maximum space margin: { top: 45, left: margin, right: margin, bottom: 20 }, // Set to 'auto' to use full available width tableWidth: 'auto', didDrawPage: (data) => { // Add page number at the bottom doc.setFontSize(8); doc.text(`Page ${data.pageNumber}`, margin, pageHeight - 10); } }); } catch (tableError) { console.error("Error creating table in PDF:", tableError); throw tableError; } // Create filename const filename = `bug_report_${format(new Date(), 'yyyy-MM-dd')}.pdf`; console.log(`Saving PDF as: ${filename}`); // Save the PDF directly try { doc.save(filename); console.log("PDF saved successfully"); } catch (saveError) { console.error("Error saving PDF:", saveError); throw saveError; } toast.success('Bugs exported to PDF successfully'); } catch (error) { console.error('Error exporting to PDF:', error); toast.error('Failed to export bugs to PDF: ' + (error instanceof Error ? error.message : 'Unknown error')); } }; // Test function to see if file download works const testFileDownload = () => { try { console.log("Testing file download"); // Create a simple text file const content = "This is a test file to verify if download works"; const blob = new Blob([content], { type: 'text/plain' }); // Create download link const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'test-download.txt'; // Add to DOM, click, and remove document.body.appendChild(a); a.click(); // Clean up setTimeout(() => { window.URL.revokeObjectURL(url); document.body.removeChild(a); console.log("Download test completed"); }, 100); toast.success("Test download initiated"); } catch (error) { console.error("Error in test download:", error); toast.error("Failed to test download"); } }; // Add a custom export dropdown component function ExportDropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setIsOpen(!isOpen); }; const handleOptionClick = (action: () => void) => { // Close dropdown first setIsOpen(false); // Then execute the action with a small delay setTimeout(() => { action(); }, 50); }; // Close dropdown when clicking outside useEffect(() => { if (!isOpen) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleOutsideClick); return () => { document.removeEventListener('mousedown', handleOutsideClick); }; }, [isOpen]); return (
{isOpen && (
handleOptionClick(() => { console.log("Excel export clicked"); handleExportToExcel(); })} > Excel
handleOptionClick(() => { console.log("PDF export clicked"); handleExportToPDF(); })} > PDF
)}
); } return ( <> {/* Quick View Dialog - Using custom modal */} setIsQuickViewOpen(false)} title={selectedBugForQuickView?.title} > {selectedBugForQuickView && (
{selectedBugForQuickView.taskCode || `BUG-${selectedBugForQuickView.id}`} {selectedBugForQuickView.createdAt && ( Created on {format(new Date(selectedBugForQuickView.createdAt), "MMMM d, yyyy")} )}

Status

{getStatusBadge(selectedBugForQuickView.status)}

Severity

{getSeverityBadge(getBugSeverity(selectedBugForQuickView))}

Version

{selectedBugForQuickView.version || "N/A"}

Assignee

{selectedBugForQuickView.assignedTo ? (
{getEmployeeInitials(selectedBugForQuickView.assignedTo)} {getEmployeeName(selectedBugForQuickView.assignedTo)}
) : ( Unassigned )}

Description

{selectedBugForQuickView.description || "No description provided."}
{/* Steps to Reproduce section */}

Steps to Reproduce

{selectedBugForQuickView.stepsToReproduce ? (
    {selectedBugForQuickView.stepsToReproduce .split('\n') .filter(Boolean) .map((step, index) => (
  1. {step.trim()}
  2. ))}
) : ( No steps to reproduce provided )}
{/* Expected Behavior section */}

Expected Behavior

{selectedBugForQuickView.expectedBehavior || No expected behavior provided }
{/* Actual Behavior section */}

Actual Behavior

{selectedBugForQuickView.actualBehavior || No actual behavior provided }
)}
{/* Time Logging Modal - Mobile Responsive */} {isLoggingTime && selectedBugForTimeLog && (
{ // Only close if clicking the backdrop setIsLoggingTime(false); setSelectedBugForTimeLog(null); setTimeLogHours(""); setRemainingHours(""); setTimeLogDescription(""); setStartTime(""); setEndTime(""); setWorkDate(new Date()); }} >
e.stopPropagation()} style={{ zIndex: 10000 }} > {/* Close button in the corner */}

Log Time

{selectedBugForTimeLog.title}

{ setStartTime(value); if (endTime) { // Calculate hours spent between start and end const hoursSpent = calculateHoursSpent(value, endTime); setTimeLogHours(hoursSpent); } }} placeholder="Select start time" className="w-full text-sm" />
{ setEndTime(value); if (startTime) { // Calculate hours spent between start and end const hoursSpent = calculateHoursSpent(startTime, value); setTimeLogHours(hoursSpent); } }} placeholder="Select end time" className="w-full text-sm" />
setTimeLogHours(e.target.value)} placeholder="Hours worked" className="text-sm h-9" />
setRemainingHours(e.target.value)} placeholder="Hours remaining" className="text-sm h-9" />