pmtool / src /pages /bugs.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
120 kB
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<string | null>(null);
return (
<DropdownContext.Provider value={{ openDropdown, setOpenDropdown }}>
{openDropdown && <div className="dropdown-overlay" onClick={() => setOpenDropdown(null)} />}
{children}
</DropdownContext.Provider>
);
}
// 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<HTMLDivElement>(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 (
<div className="relative" ref={menuRef}>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleButtonClick}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
{open && (
<div
className="fixed z-[9999] min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
style={{
top: menuPosition.top,
right: menuPosition.right,
left: menuPosition.left
}}
>
<div
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
onClick={(e) => handleAction(() => onView(bug), e)}
>
<Eye className="mr-2 h-4 w-4" />
View Details
</div>
<div
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
onClick={(e) => handleAction(() => onEdit(bug), e)}
>
<Edit2 className="mr-2 h-4 w-4" />
Edit
</div>
<div className="h-px my-1 -mx-1 bg-muted"></div>
<div
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none text-red-600 hover:bg-accent hover:text-red-600",
(isDeleting && deleteId === bug.id) && "opacity-50 cursor-not-allowed"
)}
onClick={(e) => {
if (!(isDeleting && deleteId === bug.id)) {
handleAction(() => onDelete(bug.id), e);
} else {
e.preventDefault();
e.stopPropagation();
}
}}
>
{isDeleting && deleteId === bug.id ? (
<>
<div className="h-4 w-4 mr-2 rounded-full border-b-2 border-red-600"></div>
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</>
)}
</div>
</div>
)}
</div>
);
}
// 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<HTMLDivElement>(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 (
<div className="relative w-full dropdown-container" ref={dropdownRef}>
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer dropdown-trigger",
isOpen && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<span className={!value || value === "all" ? "text-muted-foreground" : ""}>{getLabel()}</span>
<ChevronDown className={cn("h-4 w-4 shrink-0 opacity-50", isOpen && "")} />
</div>
{isOpen && (
<div
className="fixed overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md dropdown-menu custom-scrollbar dropdown-active"
style={{
top: menuPosition.top,
left: menuPosition.left,
width: menuPosition.width,
maxHeight: "300px"
}}
>
{options.map((option) => (
<div
key={option.value}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground dropdown-item",
value === option.value && "bg-accent text-accent-foreground dropdown-item active"
)}
onClick={(e) => handleOptionClick(option.value, e)}
>
{option.label}
{value === option.value && <Check className="h-4 w-4" />}
</div>
))}
</div>
)}
</div>
);
}
// 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<HTMLDivElement>(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 (
<div className="relative w-full dropdown-container" ref={dropdownRef}>
<div
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer dropdown-trigger",
isOpen && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
{value !== "all" && typeof value === "number" ? (
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{getInitials(value)}
</AvatarFallback>
</Avatar>
) : (
<User className="h-4 w-4 mr-2 opacity-50" />
)}
<span className={value === "all" ? "text-muted-foreground" : ""}>
{getEmployeeName(value)}
</span>
</div>
<ChevronDown className={cn("h-4 w-4 shrink-0 opacity-50", isOpen && "")} />
</div>
{isOpen && (
<div
className="fixed overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md dropdown-menu custom-scrollbar dropdown-active"
style={{
top: menuPosition.top,
left: menuPosition.left,
width: menuPosition.width,
maxHeight: "300px"
}}
>
<div
key="all"
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground dropdown-item",
value === "all" && "bg-accent text-accent-foreground dropdown-item active"
)}
onClick={(e) => handleOptionClick("all", e)}
>
<User className="h-4 w-4 mr-2 opacity-50" />
<span>All Assignees</span>
{value === "all" && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
{employees.map((employee) => (
<div
key={employee.id}
className={cn(
"flex cursor-pointer items-center px-2 py-1.5 hover:bg-accent hover:text-accent-foreground dropdown-item",
value === employee.id && "bg-accent text-accent-foreground dropdown-item active"
)}
onClick={(e) => handleOptionClick(employee.id, e)}
>
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px]">
{(employee.firstName.charAt(0) + employee.lastName.charAt(0)).toUpperCase()}
</AvatarFallback>
</Avatar>
<span>{employee.firstName} {employee.lastName}</span>
{value === employee.id && (
<Check className="ml-auto h-4 w-4" />
)}
</div>
))}
</div>
)}
</div>
);
}
// 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<string | "all">("all");
const [severityFilter, setSeverityFilter] = useState<string | "all">("all");
const [assigneeFilter, setAssigneeFilter] = useState<string | "all">("all");
const [projectFilter, setProjectFilter] = useState<string>("all");
// Add new filter for My/Team bugs
const [scopeFilter, setScopeFilter] = useState<"my" | "all">("my");
const [isLoading, setIsLoading] = useState(true);
const [bugs, setBugs] = useState<Task[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [teamMembers, setTeamMembers] = useState<Employee[]>([]); // Add state for team members
const [projects, setProjects] = useState<Record<number, string>>({});
const [userProjects, setUserProjects] = useState<ProjectApi[]>([]); // Add state for user's projects
const [isDeleting, setIsDeleting] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [openMenuId, setOpenMenuId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [sortField, setSortField] = useState<string>("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<Task | null>(null);
// Time logging state
const [isLoggingTime, setIsLoggingTime] = useState(false);
const [selectedBugForTimeLog, setSelectedBugForTimeLog] = useState<Task | null>(null);
const [timeLogHours, setTimeLogHours] = useState<string>("");
const [remainingHours, setRemainingHours] = useState<string>("");
const [timeLogDescription, setTimeLogDescription] = useState<string>("");
const [startTime, setStartTime] = useState<string>("");
const [endTime, setEndTime] = useState<string>("");
const [workDate, setWorkDate] = useState<Date | undefined>(new Date());
const [timeOptions, setTimeOptions] = useState<Array<{ label: string, value: string }>>([]);
// Status update state
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
const [updateStatusId, setUpdateStatusId] = useState<number | null>(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<Record<number, any[]>>({});
// 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<number, string>, 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 (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 flex items-center gap-1 px-2 font-medium">
<CheckCircle className="h-3 w-3" />
{status}
</Badge>
);
case "In Progress":
return (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200 flex items-center gap-1 px-2 font-medium">
<RefreshCw className="h-3 w-3" />
{status}
</Badge>
);
case "Re-Open":
case "Reopened":
return (
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200 flex items-center gap-1 px-2 font-medium">
<RefreshCw className="h-3 w-3" />
{status}
</Badge>
);
case "In Review":
case "Ready for QA":
return (
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200 flex items-center gap-1 px-2 font-medium">
<Clock className="h-3 w-3" />
{status}
</Badge>
);
case "Open":
case "New":
return (
<Badge variant="outline" className="bg-slate-50 text-slate-700 border-slate-200 flex items-center gap-1 px-2 font-medium">
<AlertCircle className="h-3 w-3" />
{status || "New"}
</Badge>
);
default:
return (
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-200 flex items-center gap-1 px-2 font-medium">
<AlertCircle className="h-3 w-3" />
{status || "Unknown"}
</Badge>
);
}
};
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 (
<Badge variant="outline" className="bg-red-50 border-red-200 text-red-700 flex items-center gap-1 px-2 font-medium">
<ShieldAlert className="h-3 w-3" />
{severity}
</Badge>
);
case "Critical":
return (
<Badge variant="outline" className="bg-rose-50 border-rose-200 text-rose-700 flex items-center gap-1 px-2 font-medium">
<FlameIcon className="h-3 w-3" />
{severity}
</Badge>
);
case "Major":
case "High":
return (
<Badge variant="outline" className="bg-amber-50 border-amber-200 text-amber-700 flex items-center gap-1 px-2 font-medium">
<AlertCircle className="h-3 w-3" />
{severity}
</Badge>
);
case "Minor":
case "Medium":
return (
<Badge variant="outline" className="bg-indigo-50 border-indigo-200 text-indigo-700 flex items-center gap-1 px-2 font-medium">
<Check className="h-3 w-3" />
{severity}
</Badge>
);
case "Trivial":
case "Low":
return (
<Badge variant="outline" className="bg-green-50 border-green-200 text-green-600 flex items-center gap-1 px-2 font-medium">
<Check className="h-3 w-3" />
{severity}
</Badge>
);
default:
return (
<Badge variant="outline" className="bg-gray-50 border-gray-200 text-gray-600 flex items-center gap-1 px-2 font-medium">
{severity || "Unknown"}
</Badge>
);
}
};
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 (
<span className="ml-1">
{sortOrder === "asc" ? "↑" : "↓"}
</span>
);
};
// 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<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<Button
variant="outline"
className="gap-1"
onClick={handleButtonClick}
>
<Download className="h-4 w-4" />
Export
<ChevronDown className={cn("h-4 w-4 opacity-50", isOpen && "transform rotate-180")} />
</Button>
{isOpen && (
<div
className="absolute right-0 z-50 mt-1 min-w-[160px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
>
<div
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
onClick={() => handleOptionClick(() => {
console.log("Excel export clicked");
handleExportToExcel();
})}
>
<FileSpreadsheet className="mr-2 h-4 w-4 text-green-600" />
Excel
</div>
<div
className="flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
onClick={() => handleOptionClick(() => {
console.log("PDF export clicked");
handleExportToPDF();
})}
>
<FileText className="mr-2 h-4 w-4 text-red-600" />
PDF
</div>
</div>
)}
</div>
);
}
return (
<>
{/* Quick View Dialog - Using custom modal */}
<CustomModal
isOpen={isQuickViewOpen}
onClose={() => setIsQuickViewOpen(false)}
title={selectedBugForQuickView?.title}
>
{selectedBugForQuickView && (
<div className="space-y-4">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<span className="text-xs font-mono bg-muted px-2 py-1 rounded">
{selectedBugForQuickView.taskCode || `BUG-${selectedBugForQuickView.id}`}
</span>
{selectedBugForQuickView.createdAt && (
<span className="text-xs">
Created on {format(new Date(selectedBugForQuickView.createdAt), "MMMM d, yyyy")}
</span>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Status</p>
{getStatusBadge(selectedBugForQuickView.status)}
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Severity</p>
{getSeverityBadge(getBugSeverity(selectedBugForQuickView))}
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Version</p>
<p className="font-mono text-xs">{selectedBugForQuickView.version || "N/A"}</p>
</div>
</div>
<div className="border-t pt-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Assignee</p>
{selectedBugForQuickView.assignedTo ? (
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6 bg-indigo-100">
<AvatarFallback className="text-xs bg-indigo-500 text-white">
{getEmployeeInitials(selectedBugForQuickView.assignedTo)}
</AvatarFallback>
</Avatar>
<span className="text-sm">
{getEmployeeName(selectedBugForQuickView.assignedTo)}
</span>
</div>
) : (
<span className="text-muted-foreground text-sm">Unassigned</span>
)}
</div>
</div>
<div className="border-t pt-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Description</p>
<div className="text-sm bg-muted/50 p-3 rounded-md whitespace-pre-wrap text-sm">
{selectedBugForQuickView.description || "No description provided."}
</div>
</div>
</div>
{/* Steps to Reproduce section */}
<div className="border-t pt-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Steps to Reproduce</p>
<div className="text-sm bg-muted/50 p-3 rounded-md">
{selectedBugForQuickView.stepsToReproduce ? (
<ol className="list-decimal pl-5 space-y-1 text-sm">
{selectedBugForQuickView.stepsToReproduce
.split('\n')
.filter(Boolean)
.map((step, index) => (
<li key={index}>{step.trim()}</li>
))}
</ol>
) : (
<span className="text-muted-foreground text-sm">No steps to reproduce provided</span>
)}
</div>
</div>
</div>
{/* Expected Behavior section */}
<div className="border-t pt-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Expected Behavior</p>
<div className="text-sm bg-muted/50 p-3 rounded-md whitespace-pre-wrap">
{selectedBugForQuickView.expectedBehavior ||
<span className="text-muted-foreground text-sm">No expected behavior provided</span>
}
</div>
</div>
</div>
{/* Actual Behavior section */}
<div className="border-t pt-3 mb-3">
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Actual Behavior</p>
<div className="text-sm bg-muted/50 p-3 rounded-md whitespace-pre-wrap">
{selectedBugForQuickView.actualBehavior ||
<span className="text-muted-foreground text-sm">No actual behavior provided</span>
}
</div>
</div>
</div>
<div className="border-t pt-3 flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewDetails(selectedBugForQuickView)}
className="h-8"
>
<ExternalLink className="h-3.5 w-3.5 mr-1.5" />
Open Full View
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsQuickViewOpen(false);
handleEdit(selectedBugForQuickView);
}}
className="h-8"
>
<Edit2 className="h-3.5 w-3.5 mr-1.5" />
Edit Bug
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsQuickViewOpen(false);
setSelectedBugForTimeLog(selectedBugForQuickView);
setIsLoggingTime(true);
}}
className="h-8"
>
<Timer className="h-3.5 w-3.5 mr-1.5" />
Log Time
</Button>
</div>
</div>
)}
</CustomModal>
{/* Time Logging Modal - Mobile Responsive */}
{isLoggingTime && selectedBugForTimeLog && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999] p-3 sm:p-4"
onClick={() => {
// Only close if clicking the backdrop
setIsLoggingTime(false);
setSelectedBugForTimeLog(null);
setTimeLogHours("");
setRemainingHours("");
setTimeLogDescription("");
setStartTime("");
setEndTime("");
setWorkDate(new Date());
}}
>
<div
className="bg-background rounded-lg shadow-lg w-full max-w-md max-h-[90vh] overflow-y-auto p-4 sm:p-6 relative"
onClick={(e) => e.stopPropagation()}
style={{ zIndex: 10000 }}
>
{/* Close button in the corner */}
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-2"
onClick={() => {
setIsLoggingTime(false);
setSelectedBugForTimeLog(null);
setTimeLogHours("");
setRemainingHours("");
setTimeLogDescription("");
setStartTime("");
setEndTime("");
setWorkDate(new Date());
}}
>
<X className="h-4 w-4" />
</Button>
<div className="mb-4">
<h3 className="text-base sm:text-lg font-semibold mb-1">Log Time</h3>
<p className="text-sm text-muted-foreground line-clamp-2">{selectedBugForTimeLog.title}</p>
</div>
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-2 block">Work Date</label>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<Button
variant={workDate && isSameDay(workDate, getYesterday()) ? "default" : "outline"}
className="w-full justify-center text-xs sm:text-sm h-9"
onClick={() => {
const yesterday = getYesterday();
setWorkDate(yesterday);
}}
type="button"
>
Yesterday
</Button>
<Button
variant={workDate && isSameDay(workDate, new Date()) ? "default" : "outline"}
className="w-full justify-center text-xs sm:text-sm h-9"
onClick={() => {
const today = new Date();
setWorkDate(today);
}}
type="button"
>
Today
</Button>
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal h-9 text-sm",
!workDate && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{workDate ? format(workDate, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={workDate}
onSelect={setWorkDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div className="space-y-2" style={{ zIndex: 45, position: 'relative' }}>
<label className="text-sm font-medium block">Start Time</label>
<Dropdown
options={timeOptions}
value={startTime}
onValueChange={(value) => {
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"
/>
</div>
<div className="space-y-2" style={{ zIndex: 40, position: 'relative' }}>
<label className="text-sm font-medium block">End Time</label>
<Dropdown
options={timeOptions}
value={endTime}
onValueChange={(value) => {
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"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div className="space-y-2">
<label className="text-sm font-medium block">Hours Spent</label>
<Input
type="number"
min="0"
step="0.5"
value={timeLogHours}
onChange={(e) => setTimeLogHours(e.target.value)}
placeholder="Hours worked"
className="text-sm h-9"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium block">Hours Remaining</label>
<Input
type="number"
min="0"
step="0.5"
value={remainingHours}
onChange={(e) => setRemainingHours(e.target.value)}
placeholder="Hours remaining"
className="text-sm h-9"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium block">Description</label>
<Textarea
value={timeLogDescription}
onChange={(e) => setTimeLogDescription(e.target.value)}
placeholder="What did you work on?"
rows={3}
className="text-sm resize-none"
/>
</div>
<Button
className="w-full h-10 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white text-sm font-medium"
onClick={() => handleTimeLogSubmit(selectedBugForTimeLog.id)}
>
<Clock className="h-4 w-4 mr-2" /> Log Time
</Button>
</div>
</div>
</div>
)}
<Layout>
<TooltipProvider>
<DropdownProvider>
<div className="pb-6 w-full">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800 border-b">
<div className="flex flex-col gap-4 px-3 sm:px-4 md:px-6 py-4 sm:py-6">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 sm:gap-4">
<div className="flex-1 min-w-0">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight flex items-center gap-2 mb-1">
<Bug className="h-6 w-6 sm:h-7 sm:w-7 text-indigo-600 flex-shrink-0" />
<span className="truncate">Bug Tracker</span>
</h1>
<p className="text-muted-foreground text-sm sm:text-base max-w-lg">
Track, manage, and resolve bugs and issues across your projects
</p>
</div>
{/* Mobile-first action buttons */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-2 flex-shrink-0">
<ImportTasksDialog
taskType="Bug"
projectId={undefined}
onImportComplete={handleRefreshBugs}
/>
<Button
onClick={handleCreate}
className="bg-blue-600 hover:bg-blue-700 shadow-sm w-full sm:w-auto order-1 sm:order-3"
size="sm"
>
<Plus className="mr-2 h-4 w-4" />
<span className="sm:hidden">New Bug</span>
<span className="hidden sm:inline">New Bug</span>
</Button>
{/* Export dropdown - hidden on mobile, shown on larger screens */}
<div className="relative hidden sm:block order-2 sm:order-4">
<ExportDropdown />
</div>
</div>
</div>
{/* View mode and scope filters - stacked on mobile */}
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
{/* View mode toggle */}
<div className="bg-background/80 backdrop-blur-sm border shadow-sm p-1 rounded-lg flex w-full sm:w-auto">
<Button
variant={viewMode === "dashboard" ? "default" : "ghost"}
size="sm"
onClick={() => setViewMode("dashboard")}
className={cn(
"flex-1 sm:flex-initial text-xs sm:text-sm",
viewMode === "dashboard" ? "bg-white dark:bg-gray-800 shadow-sm text-primary" : ""
)}
>
<LayoutDashboard className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
Dashboard
</Button>
<Button
variant={viewMode === "list" ? "default" : "ghost"}
size="sm"
onClick={() => setViewMode("list")}
className={cn(
"flex-1 sm:flex-initial text-xs sm:text-sm",
viewMode === "list" ? "bg-white dark:bg-gray-800 shadow-sm text-primary" : ""
)}
>
<LayoutList className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
List
</Button>
</div>
{/* Scope filter buttons */}
<div className="bg-background/80 backdrop-blur-sm border shadow-sm p-1 rounded-lg flex w-full sm:w-auto">
<Button
variant={scopeFilter === "my" ? "default" : "ghost"}
size="sm"
onClick={() => setScopeFilter("my")}
className={cn(
"flex-1 sm:flex-initial text-xs sm:text-sm",
scopeFilter === "my" ? "bg-white dark:bg-gray-800 shadow-sm text-primary" : ""
)}
>
<User className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
My Bugs
</Button>
<Button
variant={scopeFilter === "all" ? "default" : "ghost"}
size="sm"
onClick={() => setScopeFilter("all")}
className={cn(
"flex-1 sm:flex-initial text-xs sm:text-sm",
scopeFilter === "all" ? "bg-white dark:bg-gray-800 shadow-sm text-primary" : ""
)}
>
<ListFilter className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
All Bugs
</Button>
</div>
{/* Export dropdown for mobile - shown only on small screens */}
<div className="relative sm:hidden">
<ExportDropdown />
</div>
</div>
</div>
</div>
{/* Dashboard View */}
{viewMode === "dashboard" && (
<div className="px-4 md:px-6 pb-4">
<DashboardBugs bugs={bugs} employees={employees} />
</div>
)}
{/* List View */}
{viewMode === "list" && (
<div className="flex flex-col space-y-4 px-4 md:px-6 pt-4 overflow-visible">
{/* Scope header - mobile responsive */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 py-2">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2">
{scopeFilter === "my" ? (
<>
<User className="h-4 w-4 sm:h-5 sm:w-5 text-indigo-600" />
My Bugs
</>
) : (
<>
<ListFilter className="h-4 w-4 sm:h-5 sm:w-5 text-indigo-600" />
All Bugs
</>
)}
</h2>
<div className="text-sm text-muted-foreground">
{filteredAndSortedBugs().length} {filteredAndSortedBugs().length === 1 ? 'bug' : 'bugs'}
</div>
</div>
<Card className="shadow-sm">
<CardHeader className="pb-3 border-b">
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<CardTitle
className="flex items-center gap-2 cursor-pointer text-base sm:text-lg"
onClick={() => setFiltersExpanded(!filtersExpanded)}
>
<Filter className="h-4 w-4 sm:h-5 sm:w-5 text-indigo-500" />
Filter & Search
<ChevronRight className={`h-3 w-3 sm:h-4 sm:w-4 transition-transform ${filtersExpanded ? 'rotate-90' : ''}`} />
</CardTitle>
{/* Refresh button - always visible */}
<Button
variant="outline"
size="sm"
onClick={handleRefreshBugs}
disabled={isLoading}
title="Refresh bug list"
className="h-8 w-8 p-0 sm:h-9 sm:w-9"
>
{isLoading ? (
<div className="h-3 w-3 sm:h-4 sm:w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="h-3 w-3 sm:h-4 sm:w-4" />
)}
</Button>
</div>
{/* Search input - full width on mobile */}
<div className="relative w-full">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search bugs..."
className="pl-8 bg-background/50 border-muted text-sm sm:text-base"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</CardHeader>
{/* Conditionally render card content based on expanded state */}
{filtersExpanded && (
<CardContent className="pt-4">
<div className="space-y-4">
{/* Filter dropdowns - mobile-first responsive grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 sm:gap-4">
<div className="space-y-2">
<label className="text-xs sm:text-sm font-medium pl-1 text-muted-foreground">Project</label>
<Dropdown
options={[
{ label: "All Projects", value: "all" },
// Get distinct project names from both projects object and bugs
...Array.from(
new Set([
...Object.values(projects),
...bugs
.filter(bug => bug.projectName)
.map(bug => bug.projectName as string)
])
).sort().map(name => ({
label: name,
value: name
}))
]}
value={projectFilter}
onValueChange={handleProjectFilterChange}
placeholder="Filter by Project"
className="w-full text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-xs sm:text-sm font-medium pl-1 text-muted-foreground">Status</label>
<Dropdown
options={[
{ label: "All Statuses", value: "all" },
{ label: "Open", value: "Open" },
{ label: "In Progress", value: "In Progress" },
{ label: "In Review", value: "In Review" },
{ label: "Ready for QA", value: "Ready for QA" },
{ label: "Resolved", value: "Resolved" },
{ label: "Closed", value: "Closed" }
]}
value={statusFilter}
onValueChange={handleStatusFilterChange}
placeholder="Status"
className="w-full text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-xs sm:text-sm font-medium pl-1 text-muted-foreground">Severity</label>
<Dropdown
options={[
{ label: "All Severities", value: "all" },
{ label: "Critical", value: "Critical" },
{ label: "High", value: "High" },
{ label: "Medium", value: "Medium" },
{ label: "Low", value: "Low" }
]}
value={severityFilter}
onValueChange={handleSeverityFilterChange}
placeholder="Severity"
className="w-full text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-xs sm:text-sm font-medium pl-1 text-muted-foreground">Assignee</label>
<Dropdown
options={[
{ label: "All Assignees", value: "all" },
...employees.map(employee => ({ // Use all employees instead of team members
label: `${employee.firstName} ${employee.lastName}`,
value: employee.id.toString()
}))
]}
value={assigneeFilter}
onValueChange={handleAssigneeFilterChange}
placeholder="Assignee"
className="w-full text-sm"
/>
</div>
</div>
</div>
{/* Active Filters - mobile-friendly layout */}
{(statusFilter !== "all" || severityFilter !== "all" || assigneeFilter !== "all" || projectFilter !== "all" || searchQuery || scopeFilter !== "all") && (
<div className="mt-4 border-t pt-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs sm:text-sm font-medium text-muted-foreground">Active filters:</span>
<Button
variant="ghost"
size="sm"
className="text-xs sm:text-sm h-7 px-2"
onClick={() => {
setSearchQuery("");
setStatusFilter("all");
setSeverityFilter("all");
setAssigneeFilter("all");
setProjectFilter("all");
}}
>
<X className="h-3 w-3 mr-1" />
Clear All
</Button>
</div>
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{scopeFilter !== "all" && (
<Badge variant="outline" className="flex items-center gap-1 bg-indigo-50 text-indigo-700 hover:bg-indigo-100 px-2 py-1 border-indigo-200 text-xs">
<span className="font-medium">Scope:</span> My Bugs
<X
className="h-3 w-3 ml-1 cursor-pointer"
onClick={() => setScopeFilter("all")}
/>
</Badge>
)}
{statusFilter !== "all" && (
<Badge variant="outline" className="flex items-center gap-1 bg-blue-50 text-blue-700 hover:bg-blue-100 px-2 py-1 border-blue-200 text-xs">
<span className="font-medium">Status:</span> {statusFilter}
<X
className="h-3 w-3 ml-1 cursor-pointer"
onClick={() => setStatusFilter("all")}
/>
</Badge>
)}
{severityFilter !== "all" && (
<Badge variant="outline" className="flex items-center gap-1 bg-orange-50 text-orange-700 hover:bg-orange-100 px-2 py-1 border-orange-200 text-xs">
<span className="font-medium">Severity:</span> {severityFilter}
<X
className="h-3 w-3 ml-1 cursor-pointer"
onClick={() => setSeverityFilter("all")}
/>
</Badge>
)}
{projectFilter !== "all" && (
<Badge variant="outline" className="flex items-center gap-1 bg-green-50 text-green-700 hover:bg-green-100 px-2 py-1 border-green-200 text-xs">
<span className="font-medium">Project:</span>
<span className="truncate max-w-[100px]">{projectFilter}</span>
<X
className="h-3 w-3 ml-1 cursor-pointer flex-shrink-0"
onClick={() => setProjectFilter("all")}
/>
</Badge>
)}
{assigneeFilter !== "all" && (
<Badge variant="outline" className="flex items-center gap-1 bg-purple-50 text-purple-700 hover:bg-purple-100 px-2 py-1 border-purple-200 text-xs">
<span className="font-medium">Assignee:</span>
<span className="truncate max-w-[80px]">
{employees.find(emp => emp.id.toString() === assigneeFilter)?.firstName || `ID: ${assigneeFilter}`}
</span>
<X
className="h-3 w-3 ml-1 cursor-pointer flex-shrink-0"
onClick={() => setAssigneeFilter("all")}
/>
</Badge>
)}
{searchQuery && (
<Badge variant="outline" className="flex items-center gap-1 bg-gray-50 text-gray-700 hover:bg-gray-100 px-2 py-1 border-gray-200 text-xs">
<span className="font-medium">Search:</span>
<span className="truncate max-w-[100px]">{searchQuery}</span>
<X
className="h-3 w-3 ml-1 cursor-pointer flex-shrink-0"
onClick={() => setSearchQuery("")}
/>
</Badge>
)}
</div>
</div>
)}
</CardContent>
)}
</Card>
{isLoading ? (
<div className="flex items-center justify-center py-16">
<div className="flex flex-col items-center gap-3">
<div className="h-10 w-10 rounded-full border-t-2 border-indigo-600 animate-spin"></div>
<p className="text-sm text-muted-foreground">Loading Bugs...</p>
</div>
</div>
) : filteredAndSortedBugs().length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="h-16 w-16 rounded-full bg-indigo-50 flex items-center justify-center mb-4">
<Bug className="h-8 w-8 text-indigo-600" />
</div>
<h3 className="text-xl font-medium mb-2">No Bugs found</h3>
<p className="text-muted-foreground mb-6 max-w-md">
{searchQuery || statusFilter !== "all" || severityFilter !== "all" || assigneeFilter !== "all" || projectFilter !== "all"
? "Try adjusting your filters to see more results."
: "Create a new Bug to start tracking bugs and problems in your project."}
</p>
{searchQuery || statusFilter !== "all" || severityFilter !== "all" || assigneeFilter !== "all" || projectFilter !== "all" ? (
<Button
variant="outline"
className="bg-background"
onClick={() => {
setSearchQuery("");
setStatusFilter("all");
setSeverityFilter("all");
setAssigneeFilter("all");
setProjectFilter("all");
}}
>
<X className="mr-2 h-4 w-4" />
Clear All Filters
</Button>
) : (
<Button onClick={handleCreate} className="bg-indigo-600 hover:bg-indigo-700">
<Plus className="mr-2 h-4 w-4" />
Create New Bug
</Button>
)}
</div>
) : (
<>
{/* Mobile summary cards - optimized for touch */}
<div className="md:hidden mt-4 space-y-3">
{filteredAndSortedBugs()
.filter(shouldShowBug)
.map((bug, index) => (
<Card
key={bug.id}
className={`shadow-sm overflow-hidden transition-all duration-200 active:scale-[0.98] ${index % 4 === 0 ? 'border-l-4 border-l-blue-500' :
index % 4 === 1 ? 'border-l-4 border-l-purple-500' :
index % 4 === 2 ? 'border-l-4 border-l-amber-500' :
'border-l-4 border-l-green-500'
}`}
onClick={() => handleViewDetails(bug)}
>
<div className="p-3 sm:p-4 cursor-pointer">
{/* Header with title and status */}
<div className="flex justify-between items-start mb-3 gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
#{index + 1}
</span>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">
{bug.taskCode || `BUG-${bug.id}`}
</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<h3 className="font-medium text-sm leading-tight line-clamp-2 text-foreground cursor-pointer">
{bug.title}
</h3>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm z-50">
<p className="text-sm whitespace-normal">{bug.title}</p>
</TooltipContent>
</Tooltip>
</div>
{/* Status dropdown - more touch-friendly */}
<div onClick={(e) => e.stopPropagation()} className="flex-shrink-0">
<div className="min-w-[100px]">
<CustomDropdown
id={`status-dropdown-mobile-${bug.id}`}
value={bug.status}
onChange={(newStatus) => handleStatusChange(bug, newStatus)}
options={statusOptions.map(status => ({ value: status, label: status }))}
placeholder="Select status"
/>
</div>
</div>
</div>
{/* Bug details grid */}
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-muted-foreground">Severity:</span>
{getSeverityBadge(getBugSeverity(bug))}
</div>
<div className="text-xs text-muted-foreground">
{bug.createdAt ? format(new Date(bug.createdAt), "MMM d") : "N/A"}
</div>
</div>
{(bug as Task).projectName && (
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-muted-foreground">Project:</span>
<span className="text-xs truncate flex-1 text-foreground">{(bug as Task).projectName}</span>
</div>
)}
{(bug as Task).issueName && (
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-muted-foreground">Issue:</span>
<span className="text-xs truncate flex-1 text-foreground">{(bug as Task).issueName}</span>
</div>
)}
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-muted-foreground">Assignee:</span>
{bug.assignedTo ? (
<div className="flex items-center gap-1.5">
<Avatar className="h-4 w-4">
<AvatarFallback className="text-[8px] bg-indigo-500 text-white">
{getEmployeeInitials(bug.assignedTo)}
</AvatarFallback>
</Avatar>
<span className="text-xs truncate max-w-[120px] text-foreground">
{getEmployeeName(bug.assignedTo)}
</span>
</div>
) : (
<span className="text-xs text-muted-foreground">Unassigned</span>
)}
</div>
</div>
{/* Action buttons - larger touch targets */}
<div className="flex justify-between items-center pt-2 border-t">
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
handleQuickView(bug, e);
}}
>
<Eye className="h-3.5 w-3.5 mr-1 text-indigo-500" />
View
</Button>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
handleEdit(bug);
}}
>
<Edit2 className="h-3.5 w-3.5 text-amber-500" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
setSelectedBugForTimeLog(bug);
setIsLoggingTime(true);
}}
>
<Timer className="h-3.5 w-3.5 text-blue-500" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
handleDelete(bug.id);
}}
disabled={isDeleting && deleteId === bug.id}
>
{isDeleting && deleteId === bug.id ? (
<div className="h-3.5 w-3.5 rounded-full border-b-2 border-red-600 animate-spin"></div>
) : (
<Trash2 className="h-3.5 w-3.5 text-red-500" />
)}
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
{/* Collapse completed bugs toggle - mobile responsive */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2 sm:gap-4 mt-4">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCollapseCompletedBugs(!collapseCompletedBugs)}
className="flex items-center gap-1 text-xs sm:text-sm h-8"
>
<ChevronRight
className={`h-3 w-3 sm:h-4 sm:w-4 transition-transform ${!collapseCompletedBugs ? 'rotate-90' : ''}`}
/>
{collapseCompletedBugs ? 'Show Completed' : 'Hide Completed'}
</Button>
{collapseCompletedBugs && getCompletedBugsCount() > 0 && (
<span className="text-xs text-muted-foreground">
({getCompletedBugsCount()} completed {getCompletedBugsCount() === 1 ? 'bug' : 'bugs'} hidden)
</span>
)}
</div>
<div className="text-xs text-muted-foreground">
Total: {filteredAndSortedBugs().length} {filteredAndSortedBugs().length === 1 ? 'bug' : 'bugs'}
</div>
</div>
{/* Desktop table view - hidden on small screens */}
<div className="hidden md:block overflow-hidden rounded-md border mt-4 table-container">
<table className="w-full text-sm table-fixed">
<thead>
<tr className="bg-gradient-to-r from-indigo-50 via-blue-50 to-indigo-50 border-b">
<th className="px-2 py-3 text-left font-semibold text-gray-700 w-[3%]">#</th>
<th
className="px-4 py-3 text-left font-semibold cursor-pointer text-gray-700 w-[20%]"
onClick={() => handleSortChange("title")}
>
<div className="flex items-center">
Bug Title
{renderSortIcon("title")}
</div>
</th>
<th
className="px-4 py-3 text-left font-semibold cursor-pointer hidden md:table-cell text-gray-700 w-[11%]"
onClick={() => handleSortChange("status")}
>
<div className="flex items-center">
Status
{renderSortIcon("status")}
</div>
</th>
<th
className="px-4 py-3 text-left font-semibold cursor-pointer hidden lg:table-cell text-gray-700 w-[10%]"
onClick={() => handleSortChange("severity")}
>
<div className="flex items-center">
Severity
{renderSortIcon("severity")}
</div>
</th>
<th className="px-4 py-3 text-left font-semibold hidden sm:table-cell text-gray-700 w-[5%]">
ID
</th>
<th className="px-4 py-3 text-left font-semibold hidden md:table-cell text-gray-700 w-[11%]">
Project
</th>
<th
className="px-4 py-3 text-left font-semibold cursor-pointer hidden lg:table-cell text-gray-700 w-[12%]"
onClick={() => handleSortChange("assignedTo")}
>
<div className="flex items-center">
Assignee
{renderSortIcon("assignedTo")}
</div>
</th>
<th
className="px-4 py-3 text-left font-semibold cursor-pointer hidden md:table-cell text-gray-700 w-[12%]"
onClick={() => handleSortChange("createdAt")}
>
<div className="flex items-center">
Created By
{renderSortIcon("createdAt")}
</div>
</th>
<th className="px-4 py-3 text-right font-semibold text-gray-700 w-[16%]">
Actions
</th>
</tr>
</thead>
<tbody>
{filteredAndSortedBugs()
.filter(shouldShowBug)
.map((bug, index) => (
<tr key={bug.id} className={`border-t hover:bg-accent/5 group ${index % 2 === 0 ? 'bg-muted/20' : ''}`}>
<td className="px-2 py-3 text-center text-muted-foreground w-[3%]">
{index + 1}
</td>
<td className="px-4 py-3 font-medium w-[20%]">
<div
className="flex items-center cursor-pointer"
onClick={() => handleViewDetails(bug)}
>
<div className={`w-2 h-2 rounded-full mr-2 flex-shrink-0 ${bug.status === 'Completed' || bug.status === 'Closed' ? 'bg-green-500' :
bug.status === 'In Progress' ? 'bg-blue-500' :
bug.status === 'In Review' ? 'bg-purple-500' :
bug.status === 'Reopened' ? 'bg-orange-500' : 'bg-gray-500'
}`}></div>
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate underline-offset-2 group-hover:underline block">
{bug.title && bug.title.length > 10 ? `${bug.title.substring(0, 35)}...` : bug.title}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-md z-50">
<p className="text-sm whitespace-normal">{bug.title}</p>
</TooltipContent>
</Tooltip>
</div>
</td>
<td className="px-4 py-3 hidden md:table-cell w-[11%]">
{/* Status badge with dropdown for desktop */}
<CustomDropdown
id={`status-dropdown-desktop-${bug.id}`}
value={bug.status}
onChange={(newStatus) => handleStatusChange(bug, newStatus)}
options={statusOptions.map(status => ({ value: status, label: status }))}
placeholder="Select status"
/>
</td>
<td className="px-4 py-3 hidden lg:table-cell w-[10%]">
{getSeverityBadge(getBugSeverity(bug))}
</td>
<td className="px-4 py-3 hidden sm:table-cell w-[5%]">
<span className="text-xs font-mono bg-muted px-2 py-1 rounded">
{bug.taskCode || `BUG-${bug.id}`}
</span>
</td>
<td className="px-4 py-3 hidden md:table-cell w-[11%]">
<span className="truncate inline-block">
{(bug as Task).projectName || 'N/A'}
</span>
</td>
<td className="px-4 py-3 hidden lg:table-cell w-[12%]">
{bug.assignedTo ? (
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6 bg-indigo-100 flex-shrink-0">
<AvatarFallback className="text-xs bg-indigo-500 text-white">
{getEmployeeInitials(bug.assignedTo)}
</AvatarFallback>
</Avatar>
<span className="truncate">
{getEmployeeName(bug.assignedTo)}
</span>
</div>
) : (
<span className="text-muted-foreground">Unassigned</span>
)}
</td>
<td className="px-4 py-3 text-muted-foreground hidden md:table-cell w-[12%]">
<div className="flex flex-col">
<span className="text-gray-900 font-medium truncate">
{(bug as Task).createdByName || 'N/A'}
</span>
<span className="text-xs">
{bug.createdAt ? format(new Date(bug.createdAt), "MMM d, yyyy") : "N/A"}
</span>
</div>
</td>
<td className="px-4 py-3 text-right w-[16%]">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-indigo-500 hover:text-indigo-600 hover:bg-indigo-50"
onClick={(e) => handleQuickView(bug, e)}
>
<Eye className="h-4 w-4" />
<span className="sr-only">View details</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-amber-500 hover:text-amber-600 hover:bg-amber-50"
onClick={() => handleEdit(bug)}
>
<Edit2 className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-50"
onClick={(e) => {
e.stopPropagation();
setSelectedBugForTimeLog(bug);
setIsLoggingTime(true);
}}
>
<Timer className="h-4 w-4" />
<span className="sr-only">Log time</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => handleDelete(bug.id)}
disabled={isDeleting && deleteId === bug.id}
>
{isDeleting && deleteId === bug.id ? (
<div className="h-4 w-4 rounded-full border-b-2 border-red-600"></div>
) : (
<Trash2 className="h-4 w-4" />
)}
<span className="sr-only">Delete</span>
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
)}
</div>
</DropdownProvider>
</TooltipProvider>
</Layout>
</>
);
};
export default Bugs;