pmtool / src /pages /bug-detail.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
22.5 kB
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;