pmtool / src /pages /issues.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
36.4 kB
import React, { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import {
ArrowRight,
CheckCircle,
Circle,
Clock,
Filter,
MoreHorizontal,
Plus,
RefreshCw,
Search,
Pencil,
Trash2,
Triangle,
Eye,
XCircle,
AlertCircle,
FileCheck,
User,
Calendar
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Dropdown, DropdownOption } from "@/components/ui/dropdown";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import Layout from "@/components/layout/layout";
import { issuesApi, projectService, deliverablesApi, employeeService, userSessionService } from "@/lib/api";
import { IssueApi, ProjectApi, Deliverable, Employee } from "@/types";
import { useNavigate } from "react-router-dom";
import { toast } from "@/lib/custom-toast";
import { format } from "date-fns";
import ImportIssuesDialog from "@/components/issue/import-issues-dialog";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
const Issues = () => {
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const [projectFilter, setProjectFilter] = useState<number | "all">("all");
const [statusFilter, setStatusFilter] = useState<string | "all">("all");
const [typeFilter, setTypeFilter] = useState<string | "all">("all");
const [priorityFilter, setPriorityFilter] = useState<string | "all">("all");
const [displayMode, setDisplayMode] = useState<"list" | "board">("list");
const [isLoading, setIsLoading] = useState(true);
const [issues, setIssues] = useState<IssueApi[]>([]);
const [allIssues, setAllIssues] = useState<IssueApi[]>([]);
const [projects, setProjects] = useState<ProjectApi[]>([]);
const [userProjects, setUserProjects] = useState<ProjectApi[]>([]);
const [deliverables, setDeliverables] = useState<Deliverable[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
// Get current user info
const currentUser = userSessionService.getSession();
// Check if user has permission to view all projects
// Only CEO, COO, admin, and Administrator roles can see all projects
const canViewAllProjects = currentUser?.roleName &&
['CEO', 'COO', 'Admin', 'Administrator'].includes(currentUser.roleName);
// Check if user has permission to add/edit/delete issues
// Only Project Manager and Business Analyst can add, edit or delete
const canManageIssues = currentUser?.roleName &&
['Project Manager', 'CEO', 'COO', 'Admin', 'Administrator', 'Sr. Project Manager','Business Analyst'].includes(currentUser.roleName);
// By default, we only show user's projects
// Admin users can toggle to see all projects
const [showAllProjects, setShowAllProjects] = useState(false);
const statusOptions = ["Open", "In Progress", "In Review", "Completed", "Closed"];
const priorityOptions = ["Low", "Medium", "High", "Critical"];
const typeOptions = ["User Story", "Bug", "Task", "Documentation", "Enhancement"];
// Load saved filters from localStorage when component mounts
useEffect(() => {
const savedFilters = localStorage.getItem('issuesPageFilters');
if (savedFilters) {
try {
const filters = JSON.parse(savedFilters);
setSearchQuery(filters.searchQuery || "");
setProjectFilter(filters.projectFilter || "all");
setStatusFilter(filters.statusFilter || "all");
setTypeFilter(filters.typeFilter || "all");
setPriorityFilter(filters.priorityFilter || "all");
setDisplayMode(filters.displayMode || "list");
setShowAllProjects(filters.showAllProjects || false);
} catch (error) {
console.error("Error parsing saved filters:", error);
// Clear corrupted filter data
localStorage.removeItem('issuesPageFilters');
}
}
}, []);
// Save filters to localStorage whenever they change
useEffect(() => {
const filtersToSave = {
searchQuery,
projectFilter,
statusFilter,
typeFilter,
priorityFilter,
displayMode,
showAllProjects
};
localStorage.setItem('issuesPageFilters', JSON.stringify(filtersToSave));
}, [
searchQuery,
projectFilter,
statusFilter,
typeFilter,
priorityFilter,
displayMode,
showAllProjects
]);
useEffect(() => {
fetchData();
}, [showAllProjects, canViewAllProjects]);
// Set default view mode based on screen size
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 768) {
setDisplayMode("board");
} else {
setDisplayMode("list");
}
};
// Set initial value
handleResize();
// Add event listener for window resize
window.addEventListener("resize", handleResize);
// Clean up event listener
return () => window.removeEventListener("resize", handleResize);
}, []);
const fetchData = async () => {
setIsLoading(true);
try {
// Get all issues first
const issuesData = await issuesApi.getAll();
setAllIssues(issuesData);
// Get all projects for reference
const allProjectsData = await projectService.getAll();
setProjects(allProjectsData);
// If user can view all projects and has toggled that option on
if (canViewAllProjects && showAllProjects) {
setIssues(issuesData);
setUserProjects(allProjectsData);
}
// Otherwise show only user's projects (default for all users)
else if (currentUser?.employeeId) {
const userProjectsData = await projectService.getByEmployeeId(currentUser.employeeId);
setUserProjects(userProjectsData);
// Filter issues to only show those from user's projects
const filteredIssues = issuesData.filter(issue =>
userProjectsData.some(project => project.id === issue.projectId)
);
setIssues(filteredIssues);
} else {
// Fallback if somehow no employee ID
setIssues(issuesData);
setUserProjects(allProjectsData);
}
const [deliverablesData, employeesData] = await Promise.all([
deliverablesApi.getAll(),
employeeService.getAll()
]);
setDeliverables(deliverablesData);
setEmployees(employeesData);
} catch (error) {
console.error("Error fetching data:", error);
toast.error("Failed to load data. Please try again.");
} finally {
setIsLoading(false);
}
};
const filteredIssues = issues.filter((issue) => {
const matchesSearch =
issue.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
issue.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
issue.biCode?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesProject = projectFilter === "all" || issue.projectId === projectFilter;
const matchesType = typeFilter === "all" || issue.type === typeFilter;
const matchesPriority = priorityFilter === "all" || issue.priority === priorityFilter;
const matchesStatus = statusFilter === "all" || issue.status === statusFilter;
return matchesSearch && matchesProject && matchesType && matchesPriority && matchesStatus;
});
const handleDelete = async (id: number) => {
if (isDeleting) return;
setIsDeleting(true);
setDeleteId(id);
try {
await issuesApi.delete(id);
setIssues(issues.filter(issue => issue.id !== id));
toast.success("Features deleted successfully");
} catch (error) {
console.error("Error deleting issue:", error);
toast.error("Failed to delete issue. Please try again.");
} finally {
setIsDeleting(false);
setDeleteId(null);
}
};
const getEmployeeName = (id: number) => {
const employee = employees.find(emp => emp.id === id);
if (!employee) return "Unknown";
return `${employee.firstName} ${employee.lastName}`;
};
const getEmployeeInitials = (id: number) => {
const employee = employees.find(emp => emp.id === id);
if (!employee) return "?";
return `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`;
};
const getProjectName = (id: number) => {
const project = projects.find(proj => proj.id === id);
return project ? project.projectName : "Unknown Project";
};
const getDeliverableName = (id: number) => {
const deliverable = deliverables.find(del => del.id === id);
return deliverable ? deliverable.name : "Unknown Deliverable";
};
const getPriorityBadge = (priority: string) => {
let color = "";
let bgColor = "";
let icon = null;
switch (priority.toLowerCase()) {
case "low":
color = "text-blue-700";
bgColor = "bg-blue-50";
icon = <ArrowRight className="h-3 w-3 mr-1" />;
break;
case "medium":
color = "text-yellow-700";
bgColor = "bg-yellow-50";
icon = <Clock className="h-3 w-3 mr-1" />;
break;
case "high":
color = "text-orange-700";
bgColor = "bg-orange-50";
icon = <Triangle className="h-3 w-3 mr-1" />;
break;
case "critical":
color = "text-red-700";
bgColor = "bg-red-50";
icon = <Triangle className="h-3 w-3 mr-1" fill="currentColor" />;
break;
default:
color = "text-gray-700";
bgColor = "bg-gray-50";
icon = <Circle className="h-3 w-3 mr-1" />;
}
return (
<Badge variant="outline" className={cn("font-medium flex items-center gap-1 px-2 border-0", bgColor, color)}>
{icon}
{priority}
</Badge>
);
};
const getTypeBadge = (type: string) => {
let color = "";
let bgColor = "";
let icon = null;
switch (type.toLowerCase()) {
case "bug":
color = "text-red-700";
bgColor = "bg-red-50";
icon = <Triangle className="h-3 w-3 mr-1" fill="currentColor" />;
break;
case "feature":
case "user story":
color = "text-emerald-700";
bgColor = "bg-emerald-50";
icon = <CheckCircle className="h-3 w-3 mr-1" />;
break;
case "task":
color = "text-blue-700";
bgColor = "bg-blue-50";
icon = <Circle className="h-3 w-3 mr-1" />;
break;
case "enhancement":
color = "text-purple-700";
bgColor = "bg-purple-50";
icon = <ArrowRight className="h-3 w-3 mr-1" />;
break;
case "documentation":
color = "text-gray-700";
bgColor = "bg-gray-50";
icon = <Circle className="h-3 w-3 mr-1" />;
break;
default:
color = "text-gray-700";
bgColor = "bg-gray-50";
icon = <Circle className="h-3 w-3 mr-1" />;
}
return (
<Badge variant="outline" className={cn("font-medium flex items-center gap-1 px-2 border-0", bgColor, color)}>
{icon}
{type}
</Badge>
);
};
const getStatusBadge = (status: string) => {
let color = "";
let bgColor = "";
let icon = null;
switch (status?.toLowerCase()) {
case "open":
color = "text-blue-700";
bgColor = "bg-blue-50";
icon = <AlertCircle className="h-3 w-3 mr-1" />;
break;
case "in progress":
color = "text-yellow-700";
bgColor = "bg-yellow-50";
icon = <Clock className="h-3 w-3 mr-1" />;
break;
case "in review":
color = "text-purple-700";
bgColor = "bg-purple-50";
icon = <Eye className="h-3 w-3 mr-1" />;
break;
case "completed":
color = "text-emerald-700";
bgColor = "bg-emerald-50";
icon = <CheckCircle className="h-3 w-3 mr-1" />;
break;
case "closed":
color = "text-gray-700";
bgColor = "bg-gray-50";
icon = <XCircle className="h-3 w-3 mr-1" />;
break;
default:
color = "text-gray-700";
bgColor = "bg-gray-50";
icon = <Circle className="h-3 w-3 mr-1" />;
}
return (
<Badge variant="outline" className={cn("font-medium flex items-center gap-1 px-2 border-0", bgColor, color)}>
{icon}
{status || "Unknown"}
</Badge>
);
};
return (
<Layout>
<div className="container mx-auto py-4 md:py-6 px-3 md:px-6 max-w-7xl">
<div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-0">
<h1 className="text-xl md:text-2xl font-bold">Feature Management</h1>
<div className="flex items-center space-x-2 w-full sm:w-auto">
{canManageIssues && (
<>
<ImportIssuesDialog
projectId={projectFilter !== "all" ? Number(projectFilter) : undefined}
onImportComplete={fetchData}
/>
<Button
onClick={() => navigate('/new-issue')}
className="flex items-center flex-1 sm:flex-none justify-center"
size="sm"
aria-label="Create new Feature"
>
<Plus className="h-4 w-4 mr-2" />
<span className="sm:block">New Feature</span>
</Button>
</>
)}
<Button
variant="outline"
size="icon"
onClick={fetchData}
disabled={isLoading}
className="h-9 w-9"
aria-label="Refresh Features"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
</div>
<div className="bg-card border rounded-lg p-3 md:p-4 shadow-sm">
<div className="flex flex-col space-y-3 md:space-y-4">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 md:gap-4">
<div className="relative w-full max-w-full md:max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search Features..."
className="pl-9 w-full md:w-96"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 w-full md:w-auto self-end md:self-auto">
{canViewAllProjects && (
<div className="flex items-center space-x-2 mr-4">
<div className="flex items-center space-x-2">
<Switch
id="all-projects"
checked={showAllProjects}
onCheckedChange={setShowAllProjects}
/>
<Label htmlFor="all-projects" className="cursor-pointer flex items-center">
<User className="h-3.5 w-3.5 mr-1" />
<span className="text-sm">All Projects</span>
</Label>
</div>
</div>
)}
<div className="hidden md:flex bg-muted rounded-md p-1 w-full md:w-auto">
<Button
variant={displayMode === "list" ? "secondary" : "ghost"}
size="sm"
onClick={() => setDisplayMode("list")}
className="rounded-sm flex-1 md:flex-none"
aria-label="List view"
>
List
</Button>
<Button
variant={displayMode === "board" ? "secondary" : "ghost"}
size="sm"
onClick={() => setDisplayMode("board")}
className="rounded-sm flex-1 md:flex-none"
aria-label="Board view"
>
Board
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 md:gap-3">
<Dropdown
options={[
{ label: "All Projects", value: "all" },
...projects.map(project => ({
label: project.projectName,
value: project.id.toString()
}))
]}
value={projectFilter.toString()}
onValueChange={(value) => setProjectFilter(value === "all" ? "all" : parseInt(value))}
placeholder="Project"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Types", value: "all" },
...typeOptions.map(type => ({
label: type,
value: type
}))
]}
value={typeFilter.toString()}
onValueChange={(value) => setTypeFilter(value)}
placeholder="Type"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Priorities", value: "all" },
...priorityOptions.map(priority => ({
label: priority,
value: priority
}))
]}
value={priorityFilter.toString()}
onValueChange={(value) => setPriorityFilter(value)}
placeholder="Priority"
className="w-full"
/>
<Dropdown
options={[
{ label: "All Statuses", value: "all" },
...statusOptions.map(status => ({
label: status,
value: status
}))
]}
value={statusFilter.toString()}
onValueChange={(value) => setStatusFilter(value)}
placeholder="Status"
className="w-full"
/>
<Button
variant="outline"
className="w-full col-span-2 md:col-span-1"
size="sm"
onClick={() => {
setSearchQuery("");
setProjectFilter("all");
setTypeFilter("all");
setPriorityFilter("all");
setStatusFilter("all");
// Also clear the saved filters in localStorage
localStorage.removeItem('issuesPageFilters');
}}
>
Clear Filters
</Button>
</div>
</div>
</div>
{isLoading ? (
<div className="flex flex-col justify-center items-center h-60 md:h-80">
<div className="animate-spin rounded-full h-10 w-10 md:h-12 md:w-12 border-4 border-primary border-t-transparent mb-3 md:mb-4"></div>
<p className="text-sm md:text-base text-muted-foreground">Loading Features...</p>
</div>
) : filteredIssues.length === 0 ? (
<Card className="border shadow-sm">
<CardContent className="flex flex-col items-center justify-center py-10 md:py-16">
<div className="rounded-full bg-muted p-3 md:p-4 mb-3 md:mb-4">
<Search className="h-6 w-6 md:h-8 md:w-8 text-muted-foreground" />
</div>
<h3 className="text-lg md:text-xl font-medium mb-1 md:mb-2">No Features found</h3>
<p className="text-sm text-muted-foreground text-center max-w-md mb-4 md:mb-6 px-2">
We couldn't find any Features matching your current filters or search criteria.
</p>
<Button
onClick={() => {
setSearchQuery("");
setProjectFilter("all");
setTypeFilter("all");
setPriorityFilter("all");
setStatusFilter("all");
}}
size="sm"
className="px-3 md:px-4"
>
Reset all filters
</Button>
</CardContent>
</Card>
) : displayMode === "list" ? (
<Card className="border shadow-sm overflow-hidden">
<CardHeader className="py-3 md:py-4 bg-muted/50">
<CardTitle className="text-base md:text-lg font-medium flex flex-col sm:flex-row sm:justify-between sm:items-center gap-1">
<span>Features ({filteredIssues.length})</span>
<span className="text-xs sm:text-sm font-normal text-muted-foreground">
{canViewAllProjects && showAllProjects
? "Showing all Features"
: "Showing only your project Features"} ({filteredIssues.length} of {allIssues.length})
</span>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto -mx-3 sm:mx-0">
<table className="w-full border-collapse min-w-[640px]">
<thead>
<tr className="border-b bg-muted/30 text-left">
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground whitespace-nowrap">#</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Title</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Project</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Type</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Priority</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Status</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Assigned To</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">Start Date</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground">End Date</th>
<th className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm font-medium text-muted-foreground text-right">Actions</th>
</tr>
</thead>
<tbody>
{filteredIssues.map((issue, index) => (
<tr key={issue.id}
className="border-b hover:bg-muted/30 cursor-pointer transition-colors"
onClick={() => navigate(`/issues/${issue.id}`)}
>
<td className="py-2 md:py-3 px-3 md:px-4 text-xs md:text-sm text-muted-foreground font-mono">{index + 1}</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<div className="text-sm md:font-medium">{issue.title}</div>
<div className="text-xs text-muted-foreground mt-0.5 md:mt-1 line-clamp-1">
{issue.description}
</div>
</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<div className="flex flex-col">
<span className="text-xs md:text-sm">{getProjectName(issue.projectId)}</span>
<span className="text-xs text-muted-foreground">{getDeliverableName(issue.deliverablesId)}</span>
</div>
</td>
<td className="py-2 md:py-3 px-3 md:px-4">{getTypeBadge(issue.type)}</td>
<td className="py-2 md:py-3 px-3 md:px-4">{getPriorityBadge(issue.priority)}</td>
<td className="py-2 md:py-3 px-3 md:px-4">{getStatusBadge(issue.status)}</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<div className="flex items-center">
<Avatar className="h-6 w-6 md:h-7 md:w-7 mr-2 border">
<AvatarFallback>{getEmployeeInitials(issue.assignedTo)}</AvatarFallback>
</Avatar>
<span className="text-xs md:text-sm">{getEmployeeName(issue.assignedTo)}</span>
</div>
</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<span className="text-xs md:text-sm text-muted-foreground">
{issue.startDate ? format(new Date(issue.startDate), "MMM d, yyyy") : "-"}
</span>
</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<span className="text-xs md:text-sm text-muted-foreground">
{issue.endDate ? format(new Date(issue.endDate), "MMM d, yyyy") : "-"}
</span>
</td>
<td className="py-2 md:py-3 px-3 md:px-4">
<div className="flex items-center justify-end space-x-1" onClick={(e) => e.stopPropagation()}>
{canManageIssues ? (
<>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
navigate(`/issues/edit/${issue.id}`);
}}
className="h-7 w-7 md:h-8 md:w-8"
>
<Pencil className="h-3 w-3 md:h-4 md:w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleDelete(issue.id);
}}
disabled={isDeleting && deleteId === issue.id}
className="h-7 w-7 md:h-8 md:w-8"
>
{isDeleting && deleteId === issue.id ? (
<div className="h-3 w-3 md:h-4 md:w-4 rounded-full border-2 border-b-transparent border-primary animate-spin"></div>
) : (
<Trash2 className="h-3 w-3 md:h-4 md:w-4 text-destructive" />
)}
</Button>
</>
) : (
<span className="text-xs text-muted-foreground">No actions</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-5">
{filteredIssues.map((issue) => (
<Card
key={issue.id}
className="overflow-hidden border hover:shadow-md transition-all hover:-translate-y-1 cursor-pointer group"
onClick={() => navigate(`/issues/${issue.id}`)}
>
<div className="h-1.5" style={{
backgroundColor: issue.priority.toLowerCase() === 'critical' ? 'rgb(239, 68, 68)' :
issue.priority.toLowerCase() === 'high' ? 'rgb(249, 115, 22)' :
issue.priority.toLowerCase() === 'medium' ? 'rgb(234, 179, 8)' :
'rgb(59, 130, 246)'
}}></div>
<CardHeader className="p-3 pb-1.5 md:p-4 md:pb-2">
<div className="flex justify-between items-start">
<div className="flex flex-col">
<div className="flex flex-wrap gap-1.5 md:gap-2 mb-1.5 items-center">
{getTypeBadge(issue.type)}
{getPriorityBadge(issue.priority)}
</div>
<CardTitle className="text-sm md:text-base font-semibold mb-1 group-hover:text-primary transition-colors">
{issue.title}
</CardTitle>
<div className="flex items-center text-xs text-muted-foreground gap-1.5 md:gap-2">
<span className="font-mono">#{issue.id}</span>
<span></span>
<span>{getProjectName(issue.projectId)}</span>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="icon" className="h-7 w-7 md:h-8 md:w-8 opacity-0 group-hover:opacity-100 transition-opacity">
<MoreHorizontal className="h-3.5 w-3.5 md:h-4 md:w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/issues/${issue.id}`);
}}
>
<Eye className="mr-2 h-4 w-4" /> View Details
</DropdownMenuItem>
{canManageIssues && (
<>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/issues/edit/${issue.id}`);
}}
>
<Pencil className="mr-2 h-4 w-4" /> Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDelete(issue.id);
}}
disabled={isDeleting && deleteId === issue.id}
>
<Trash2 className="mr-2 h-4 w-4" />
{isDeleting && deleteId === issue.id ? "Deleting..." : "Delete"}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="p-3 pt-1.5 md:p-4 md:pt-2">
<div className="mb-2 md:mb-3">{getStatusBadge(issue.status)}</div>
<p className="text-xs md:text-sm text-muted-foreground line-clamp-2 mb-4 md:mb-6 min-h-[32px] md:min-h-[40px]">
{issue.description || "No description provided"}
</p>
{(issue.startDate || issue.endDate) && (
<div className="flex items-center gap-3 mb-3 text-xs text-muted-foreground">
{issue.startDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Start: {format(new Date(issue.startDate), "MMM d, yyyy")}</span>
</div>
)}
{issue.endDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>End: {format(new Date(issue.endDate), "MMM d, yyyy")}</span>
</div>
)}
</div>
)}
<div className="flex justify-between items-center mt-auto border-t pt-2 md:pt-3">
<div className="flex items-center">
<Avatar className="h-6 w-6 md:h-7 md:w-7 border">
<AvatarFallback>{getEmployeeInitials(issue.assignedTo)}</AvatarFallback>
</Avatar>
<div className="ml-2">
<div className="text-[10px] md:text-xs text-muted-foreground">Assigned to</div>
<div className="text-xs md:text-sm font-medium">{getEmployeeName(issue.assignedTo)}</div>
</div>
</div>
<div>
<div className="text-[10px] md:text-xs text-muted-foreground text-right">Reported by</div>
<div className="text-xs md:text-sm text-right">{getEmployeeName(issue.reportedBy)}</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</div>
</Layout>
);
};
export default Issues;