Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } from "react"; | |
| import { useNavigate, useParams, Link } from "react-router-dom"; | |
| import { tasksApi, Task } from "@/services/tasksApi"; | |
| import { employeeApi } from "@/services/employeeApi"; | |
| import { toast } from "sonner"; | |
| import { | |
| ChevronRight, | |
| Home, | |
| Calendar, | |
| Clock, | |
| Edit, | |
| FileText, | |
| AlertCircle, | |
| CheckCircle, | |
| RefreshCw, | |
| Bug, | |
| Code, | |
| CheckSquare, | |
| XCircle, | |
| AlertTriangle, | |
| GitBranch, | |
| Link as LinkIcon, | |
| Monitor, | |
| Info, | |
| Paperclip, | |
| MessageSquare, | |
| History | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { format } from "date-fns"; | |
| import Layout from "../components/layout/layout"; | |
| import { useQuery } from "@tanstack/react-query"; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import TaskTimeLogCard from "@/components/tasks/TaskTimeLogCard"; | |
| import TaskCommentSection from "@/components/tasks/TaskCommentSection"; | |
| import TaskAttachmentsSection from "@/components/tasks/TaskAttachmentsSection"; | |
| const BugDetailPage = () => { | |
| const navigate = useNavigate(); | |
| const { id } = useParams(); | |
| const [bug, setBug] = useState<Task | undefined>(); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [activeTab, setActiveTab] = useState("details"); | |
| // Fetch employees data | |
| const { | |
| data: employees = [], | |
| isLoading: isLoadingEmployees | |
| } = useQuery({ | |
| queryKey: ["employees"], | |
| queryFn: () => employeeApi.getAll(), | |
| }); | |
| useEffect(() => { | |
| if (id) { | |
| fetchBug(); | |
| } | |
| }, [id]); | |
| const fetchBug = async () => { | |
| try { | |
| setIsLoading(true); | |
| const bugData = await tasksApi.getById(Number(id)); | |
| // Check if this is actually a bug type | |
| if (bugData && bugData.type === "Bug") { | |
| setBug(bugData); | |
| } else { | |
| toast.error("The requested item is not a bug"); | |
| navigate("/bugs"); | |
| } | |
| } catch (error) { | |
| console.error("Error fetching bug:", error); | |
| toast.error("Failed to load bug data"); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleEdit = () => { | |
| navigate(`/bugs/${id}/edit`); | |
| }; | |
| const handleBack = () => { | |
| navigate(-1); | |
| }; | |
| // Get assignee information | |
| const getAssigneeInfo = (userId: number | null) => { | |
| if (!userId) return { initials: "NA", name: "Unassigned" }; | |
| const userIdString = userId.toString(); | |
| const employee = employees.find(emp => emp.id.toString() === userIdString); | |
| if (employee) { | |
| const initials = `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase(); | |
| return { | |
| initials, | |
| name: `${employee.firstName} ${employee.lastName}` | |
| }; | |
| } | |
| return { initials: "US", name: `User ${userId}` }; | |
| }; | |
| // Helper to get status badge | |
| const getStatusBadge = (status: string) => { | |
| switch (status) { | |
| case "Completed": | |
| case "Closed": | |
| return ( | |
| <Badge variant="default" className="bg-green-500 hover:bg-green-600 text-white flex items-center gap-1"> | |
| <CheckCircle className="h-3 w-3" /> | |
| {status} | |
| </Badge> | |
| ); | |
| case "In Progress": | |
| return ( | |
| <Badge variant="default" className="bg-blue-500 hover:bg-blue-600 text-white flex items-center gap-1"> | |
| <RefreshCw className="h-3 w-3" /> | |
| {status} | |
| </Badge> | |
| ); | |
| case "In Review": | |
| return ( | |
| <Badge variant="default" className="bg-purple-500 hover:bg-purple-600 text-white flex items-center gap-1"> | |
| <Clock className="h-3 w-3" /> | |
| {status} | |
| </Badge> | |
| ); | |
| case "New": | |
| default: | |
| return ( | |
| <Badge variant="default" className="bg-gray-500 hover:bg-gray-600 text-white flex items-center gap-1"> | |
| <AlertCircle className="h-3 w-3" /> | |
| {status || "New"} | |
| </Badge> | |
| ); | |
| } | |
| }; | |
| // Helper to get severity badge | |
| const getSeverityBadge = (severity: string) => { | |
| switch (severity?.toLowerCase()) { | |
| case "blocker": | |
| return ( | |
| <Badge variant="outline" className="border-red-700 text-red-700 flex items-center gap-1"> | |
| <AlertTriangle className="h-3 w-3 mr-1" /> | |
| {severity} | |
| </Badge> | |
| ); | |
| case "critical": | |
| return ( | |
| <Badge variant="outline" className="border-red-500 text-red-500 flex items-center gap-1"> | |
| <AlertTriangle className="h-3 w-3 mr-1" /> | |
| {severity} | |
| </Badge> | |
| ); | |
| case "major": | |
| return ( | |
| <Badge variant="outline" className="border-orange-500 text-orange-500 flex items-center gap-1"> | |
| <AlertCircle className="h-3 w-3 mr-1" /> | |
| {severity} | |
| </Badge> | |
| ); | |
| case "minor": | |
| return ( | |
| <Badge variant="outline" className="border-yellow-500 text-yellow-500 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {severity} | |
| </Badge> | |
| ); | |
| case "trivial": | |
| return ( | |
| <Badge variant="outline" className="border-green-500 text-green-500 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {severity} | |
| </Badge> | |
| ); | |
| default: | |
| return ( | |
| <Badge variant="outline" className="border-gray-500 text-gray-500 flex items-center gap-1"> | |
| {severity || "Not Set"} | |
| </Badge> | |
| ); | |
| } | |
| }; | |
| // Helper to get priority badge | |
| const getPriorityBadge = (priority: string) => { | |
| switch (priority?.toLowerCase()) { | |
| case "critical": | |
| return ( | |
| <Badge variant="outline" className="border-red-600 text-red-600 flex items-center gap-1"> | |
| <AlertCircle className="h-3 w-3 mr-1" /> | |
| {priority} | |
| </Badge> | |
| ); | |
| case "high": | |
| return ( | |
| <Badge variant="outline" className="border-orange-600 text-orange-600 flex items-center gap-1"> | |
| <AlertCircle className="h-3 w-3 mr-1" /> | |
| {priority} | |
| </Badge> | |
| ); | |
| case "medium": | |
| return ( | |
| <Badge variant="outline" className="border-yellow-600 text-yellow-600 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {priority} | |
| </Badge> | |
| ); | |
| case "low": | |
| return ( | |
| <Badge variant="outline" className="border-green-600 text-green-600 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {priority} | |
| </Badge> | |
| ); | |
| default: | |
| return ( | |
| <Badge variant="outline" className="border-gray-500 text-gray-500 flex items-center gap-1"> | |
| {priority || "Not Set"} | |
| </Badge> | |
| ); | |
| } | |
| }; | |
| // Helper to get bug type badge | |
| const getBugTypeBadge = (type: string) => { | |
| switch (type?.toLowerCase()) { | |
| case "bug": | |
| return ( | |
| <Badge variant="outline" className="border-red-400 text-red-400 flex items-center gap-1"> | |
| <Bug className="h-3 w-3 mr-1" /> | |
| {type} | |
| </Badge> | |
| ); | |
| case "feature": | |
| return ( | |
| <Badge variant="outline" className="border-blue-400 text-blue-400 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {type} | |
| </Badge> | |
| ); | |
| case "enhancement": | |
| return ( | |
| <Badge variant="outline" className="border-green-400 text-green-400 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {type} | |
| </Badge> | |
| ); | |
| case "task": | |
| return ( | |
| <Badge variant="outline" className="border-purple-400 text-purple-400 flex items-center gap-1"> | |
| <Info className="h-3 w-3 mr-1" /> | |
| {type} | |
| </Badge> | |
| ); | |
| default: | |
| return ( | |
| <Badge variant="outline" className="border-gray-500 text-gray-500 flex items-center gap-1"> | |
| {type || "Unknown"} | |
| </Badge> | |
| ); | |
| } | |
| }; | |
| // Format date for display | |
| const formatDate = (dateString: string | null) => { | |
| if (!dateString) return "Not set"; | |
| try { | |
| return format(new Date(dateString), "MMM dd, yyyy"); | |
| } catch (error) { | |
| return "Invalid date"; | |
| } | |
| }; | |
| if (isLoading || isLoadingEmployees) { | |
| return ( | |
| <Layout> | |
| <div className="h-full flex items-center justify-center p-4"> | |
| <div className="flex flex-col items-center gap-2"> | |
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> | |
| <p className="text-sm text-muted-foreground">Loading bug details...</p> | |
| </div> | |
| </div> | |
| </Layout> | |
| ); | |
| } | |
| if (!bug) { | |
| return ( | |
| <Layout> | |
| <div className="h-full flex flex-col items-center justify-center p-4"> | |
| <h2 className="text-2xl font-bold mb-2">Bug Not Found</h2> | |
| <p className="text-muted-foreground mb-4"> | |
| The bug you're looking for doesn't exist or has been removed. | |
| </p> | |
| <Button onClick={handleBack}>Back to Bugs</Button> | |
| </div> | |
| </Layout> | |
| ); | |
| } | |
| // Get assignee details | |
| const { initials: assigneeInitials, name: assigneeName } = getAssigneeInfo(bug.assignedTo); | |
| return ( | |
| <Layout> | |
| <div className="pb-6 w-full animate-fadeIn"> | |
| {/* Breadcrumb Navigation */} | |
| <nav className="flex items-center space-x-2 text-sm text-muted-foreground overflow-x-auto pb-2 mb-4 px-4 md:px-6"> | |
| <Link to="/" className="flex items-center hover:text-foreground transition-colors flex-shrink-0"> | |
| <Home className="h-4 w-4 mr-1" /> | |
| Home | |
| </Link> | |
| <ChevronRight className="h-4 w-4 flex-shrink-0" /> | |
| <Link to="/bugs" className="hover:text-foreground transition-colors flex-shrink-0"> | |
| Bugs | |
| </Link> | |
| <ChevronRight className="h-4 w-4 flex-shrink-0" /> | |
| <span className="text-foreground font-medium flex-shrink-0">Bug Details</span> | |
| </nav> | |
| <div className="px-4 md:px-6 space-y-6 max-w-7xl mx-auto"> | |
| {/* Header Section with background */} | |
| <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4 sm:p-6"> | |
| <div className="flex flex-col gap-4"> | |
| <div className="flex flex-col sm:flex-row sm:justify-between sm:flex-wrap gap-4"> | |
| <div className="space-y-1 flex-1"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <h1 className="text-xl md:text-3xl font-bold tracking-tight break-words">{bug.title}</h1> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2 mt-2"> | |
| {getStatusBadge(bug.status)} | |
| {getBugTypeBadge(bug.type)} | |
| {bug.priority && getPriorityBadge(bug.priority)} | |
| {bug.severity && getSeverityBadge(bug.severity)} | |
| <span className="text-sm text-muted-foreground ml-2"> | |
| {bug.taskCode || `BUG-${bug.id}`} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 shrink-0"> | |
| <Button variant="outline" onClick={handleBack} className="flex-1 sm:flex-auto"> | |
| Back | |
| </Button> | |
| <Button onClick={handleEdit} className="bg-blue-600 hover:bg-blue-700 flex-1 sm:flex-auto"> | |
| <Edit className="h-4 w-4 mr-2" /> | |
| Edit Bug | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3"> | |
| <div className="flex items-center gap-3 px-3 sm:px-4 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg"> | |
| <Avatar className="h-10 w-10 border-2 border-primary/10"> | |
| <AvatarFallback className="bg-primary/10 text-primary"> | |
| {assigneeInitials} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div> | |
| <p className="text-xs text-muted-foreground">Assigned To</p> | |
| <p className="font-medium">{assigneeName}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3 px-3 sm:px-4 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg"> | |
| <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"> | |
| <Calendar className="h-5 w-5 text-primary" /> | |
| </div> | |
| <div> | |
| <p className="text-xs text-muted-foreground">Created</p> | |
| <p className="font-medium">{formatDate(bug.createdAt || "")}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3 px-3 sm:px-4 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg"> | |
| <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"> | |
| <GitBranch className="h-5 w-5 text-primary" /> | |
| </div> | |
| <div> | |
| <p className="text-xs text-muted-foreground">Version</p> | |
| <p className="font-medium">{bug.version || "Not specified"}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3 px-3 sm:px-4 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg"> | |
| <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"> | |
| <Monitor className="h-5 w-5 text-primary" /> | |
| </div> | |
| <div> | |
| <p className="text-xs text-muted-foreground">Environment</p> | |
| <p className="font-medium">{bug.environment || "Not specified"}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Tab Content Section */} | |
| <Tabs defaultValue="details" className="w-full" onValueChange={setActiveTab}> | |
| <TabsList className="grid grid-cols-3 md:grid-cols-6 mb-4 overflow-x-auto"> | |
| <TabsTrigger value="details" className="text-xs md:text-sm"> | |
| <FileText className="h-4 w-4 mr-1 md:mr-2" /> | |
| Details | |
| </TabsTrigger> | |
| <TabsTrigger value="steps" className="text-xs md:text-sm"> | |
| <CheckSquare className="h-4 w-4 mr-1 md:mr-2" /> | |
| Steps | |
| </TabsTrigger> | |
| <TabsTrigger value="resolution" className="text-xs md:text-sm"> | |
| <Bug className="h-4 w-4 mr-1 md:mr-2" /> | |
| Resolution | |
| </TabsTrigger> | |
| <TabsTrigger value="attachments" className="text-xs md:text-sm"> | |
| <Paperclip className="h-4 w-4 mr-1 md:mr-2" /> | |
| Files | |
| </TabsTrigger> | |
| <TabsTrigger value="timelogs" className="text-xs md:text-sm"> | |
| <History className="h-4 w-4 mr-1 md:mr-2" /> | |
| Time Logs | |
| </TabsTrigger> | |
| <TabsTrigger value="discussion" className="text-xs md:text-sm"> | |
| <MessageSquare className="h-4 w-4 mr-1 md:mr-2" /> | |
| Discussion | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="details" className="space-y-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <FileText className="h-4 w-4 mr-2" /> | |
| Bug Description | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="prose dark:prose-invert max-w-none"> | |
| {bug.description ? ( | |
| <div className="whitespace-pre-wrap">{bug.description}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">No description provided.</p> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {bug.url && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <LinkIcon className="h-4 w-4 mr-2" /> | |
| Related URL | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <a | |
| href={bug.url.startsWith('http') ? bug.url : `https://${bug.url}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-600 dark:text-blue-400 hover:underline" | |
| > | |
| {bug.url} | |
| </a> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {bug.inputs && ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <Code className="h-4 w-4 mr-2" /> | |
| Test Data / Inputs | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="bg-muted p-3 rounded-md font-mono text-sm whitespace-pre-wrap"> | |
| {bug.inputs} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| </TabsContent> | |
| <TabsContent value="steps" className="space-y-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <CheckSquare className="h-4 w-4 mr-2" /> | |
| Steps to Reproduce | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {bug.stepsToReproduce ? ( | |
| <div className="whitespace-pre-wrap">{bug.stepsToReproduce}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">No reproduction steps provided.</p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <CheckCircle className="h-4 w-4 mr-2" /> | |
| Expected Behavior | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {bug.expectedBehavior ? ( | |
| <div className="whitespace-pre-wrap">{bug.expectedBehavior}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">No expected behavior specified.</p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <XCircle className="h-4 w-4 mr-2" /> | |
| Actual Behavior | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {bug.actualBehavior ? ( | |
| <div className="whitespace-pre-wrap">{bug.actualBehavior}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">No actual behavior specified.</p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="resolution" className="space-y-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <AlertCircle className="h-4 w-4 mr-2" /> | |
| Root Cause | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {bug.rootCause ? ( | |
| <div className="whitespace-pre-wrap">{bug.rootCause}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">Root cause analysis not provided yet.</p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-md flex items-center"> | |
| <CheckCircle className="h-4 w-4 mr-2" /> | |
| Resolution | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {bug.resolution ? ( | |
| <div className="whitespace-pre-wrap">{bug.resolution}</div> | |
| ) : ( | |
| <p className="text-muted-foreground italic">Resolution details not provided yet.</p> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="attachments" className="space-y-4"> | |
| <TaskAttachmentsSection taskId={bug.id} /> | |
| </TabsContent> | |
| <TabsContent value="timelogs" className="space-y-4"> | |
| <TaskTimeLogCard taskId={bug.id} /> | |
| </TabsContent> | |
| <TabsContent value="discussion" className="space-y-4"> | |
| <TaskCommentSection taskId={bug.id} /> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| </div> | |
| </Layout> | |
| ); | |
| }; | |
| export default BugDetailPage; |