pmtool / src /components /dashboard /dashboard-bugs.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
17.2 kB
import React from "react";
import {
AlertCircle,
Bug,
CheckCircle,
Clock,
Filter,
FlameIcon,
PieChart,
RefreshCw,
ShieldAlert,
ThumbsUp
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import ChartCard from "./chart-card";
import StatsCard from "./stats-card";
import { Task } from "@/services/tasksApi";
import { Employee } from "@/types";
import "@/styles/dashboard.css";
// 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 || '';
};
interface DashboardBugsProps {
bugs: Task[];
employees: Employee[];
}
const DashboardBugs: React.FC<DashboardBugsProps> = ({ bugs, employees }) => {
// Calculate stats for the dashboard
const totalBugs = bugs.length;
const openBugs = bugs.filter(bug =>
["New", "Open", "In Progress", "Reopened"].includes(bug.status)
).length;
const resolvedBugs = bugs.filter(bug =>
["Completed", "Resolved", "Closed"].includes(bug.status)
).length;
const criticalBugs = bugs.filter(bug =>
["Blocker", "Critical"].includes(getBugSeverity(bug))
).length;
// Get top assignees by bug count
const assigneeCounts = bugs.reduce((acc, bug) => {
if (bug.assignedTo) {
acc[bug.assignedTo] = (acc[bug.assignedTo] || 0) + 1;
}
return acc;
}, {} as Record<number, number>);
const topAssignees = Object.entries(assigneeCounts)
.map(([id, count]) => ({
id: parseInt(id),
count
}))
.sort((a, b) => b.count - a.count)
.slice(0, 5)
.map(item => {
const employee = employees.find(emp => emp.id === item.id);
return {
id: item.id,
name: employee ? `${employee.firstName} ${employee.lastName}` : `User ${item.id}`,
initials: employee ?
`${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase() :
"UN",
count: item.count
};
});
// Prepare data for charts
const statusChartData = [{
name: "Open",
value: bugs.filter(bug => bug.status === "Open").length,
}, {
name: "In Progress",
value: bugs.filter(bug => bug.status === "In Progress").length,
}, {
name: "In Review",
value: bugs.filter(bug => bug.status === "In Review").length,
}, {
name: "Completed",
value: bugs.filter(bug => bug.status === "Completed").length,
}, {
name: "Closed",
value: bugs.filter(bug => bug.status === "Closed").length,
}];
const severityChartData = [{
name: "Blocker",
value: bugs.filter(bug => getBugSeverity(bug) === "Blocker").length,
}, {
name: "Critical",
value: bugs.filter(bug => getBugSeverity(bug) === "Critical").length,
}, {
name: "Major",
value: bugs.filter(bug => getBugSeverity(bug) === "Major").length,
}, {
name: "Minor",
value: bugs.filter(bug => getBugSeverity(bug) === "Minor").length,
}, {
name: "Trivial",
value: bugs.filter(bug => getBugSeverity(bug) === "Trivial").length,
}];
const trendData = Array(7).fill(0).map((_, i) => {
const date = new Date();
date.setDate(date.getDate() - (6 - i));
// Count bugs created on this date
const count = bugs.filter(bug => {
if (!bug.createdAt) return false;
const bugDate = new Date(bug.createdAt);
return bugDate.toDateString() === date.toDateString();
}).length;
return {
name: date.toLocaleDateString('en-US', { weekday: 'short' }),
value: count
};
});
const getStatusBadge = (status: string) => {
switch (status) {
case "Completed":
case "Closed":
case "Resolved":
return (
<Badge variant="default" className="bg-green-500 hover:bg-green-600 text-white flex items-center gap-1 badge-status-completed badge-glow">
<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 badge-status-in-progress badge-glow">
<RefreshCw className="h-3 w-3" />
{status}
</Badge>
);
case "Reopened":
case "Re-Open":
return (
<Badge variant="default" className="bg-orange-500 hover:bg-orange-600 text-white flex items-center gap-1 badge-status-open badge-glow">
<RefreshCw className="h-3 w-3" />
{status}
</Badge>
);
case "In Review":
case "Ready for QA":
return (
<Badge variant="default" className="bg-purple-500 hover:bg-purple-600 text-white flex items-center gap-1 badge-status-in-review badge-glow">
<Clock className="h-3 w-3" />
{status}
</Badge>
);
case "Open":
case "New":
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}
</Badge>
);
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 || "Unknown"}
</Badge>
);
}
};
const getSeverityBadge = (severity: string | Task) => {
// 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="border-red-700 text-red-700 flex items-center gap-1 badge-severity-high">
<ShieldAlert className="h-3 w-3" />
{severity}
</Badge>
);
case "Critical":
return (
<Badge variant="outline" className="border-red-500 text-red-500 flex items-center gap-1 badge-severity-high">
<FlameIcon className="h-3 w-3" />
{severity}
</Badge>
);
case "Major":
return (
<Badge variant="outline" className="border-amber-500 text-amber-500 flex items-center gap-1 badge-severity-medium">
<AlertCircle className="h-3 w-3" />
{severity}
</Badge>
);
case "Minor":
return (
<Badge variant="outline" className="border-green-500 text-green-500 flex items-center gap-1 badge-severity-low">
<ThumbsUp className="h-3 w-3" />
{severity}
</Badge>
);
case "Trivial":
return (
<Badge variant="outline" className="border-green-300 text-green-400 flex items-center gap-1 badge-severity-low">
<ThumbsUp className="h-3 w-3" />
{severity}
</Badge>
);
default:
return (
<Badge variant="outline" className="border-gray-500 text-gray-500 flex items-center gap-1">
{severity || "Unknown"}
</Badge>
);
}
};
return ( <div className="space-y-4 md:space-y-6 card-fade-in"> <h2 className="text-xl md:text-2xl font-bold">Bug Tracking Dashboard</h2> {/* Stats Overview */} <div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-4"> <StatsCard title="Total Bugs" value={totalBugs} icon={Bug} iconColor="indigo" className="bg-gradient-to-br from-indigo-50 to-indigo-100 border-indigo-200 shadow-sm hover-scale text-primary" />
<StatsCard
title="Open Bugs"
value={openBugs}
icon={AlertCircle}
iconColor="amber"
className="bg-gradient-to-br from-amber-50 to-amber-100 border-amber-200 shadow-sm hover-scale text-primary"
change={{
value: totalBugs ? Math.round((openBugs / totalBugs) * 100) : 0,
positive: false
}}
/>
<StatsCard
title="Resolved Bugs"
value={resolvedBugs}
icon={CheckCircle}
iconColor="green"
className="bg-gradient-to-br from-green-50 to-green-100 border-green-200 shadow-sm hover-scale text-primary"
change={{
value: totalBugs ? Math.round((resolvedBugs / totalBugs) * 100) : 0,
positive: true
}}
/>
<StatsCard
title="Critical Bugs"
value={criticalBugs}
icon={ShieldAlert}
iconColor="red"
className="bg-gradient-to-br from-red-50 to-red-100 border-red-200 shadow-sm hover-scale text-primary"
change={{
value: totalBugs ? Math.round((criticalBugs / totalBugs) * 100) : 0,
positive: false
}}
/>
</div>
{/* Charts Row */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-3 md:gap-6"> <div className="chart-container"> <ChartCard title="Bug Status Distribution" data={[statusChartData]} colors={["#6366F1", "#3B82F6", "#8B5CF6", "#22C55E", "#64748B"]} type="bar" showGrid={true} className="shadow-sm hover-scale text-primary" /> </div> <div className="chart-container"> <ChartCard title="Bug Severity Distribution" data={[severityChartData]} colors={["#B91C1C", "#EF4444", "#F59E0B", "#22C55E", "#86EFAC"]} type="bar" showGrid={true} className="shadow-sm hover-scale text-primary" /> </div> </div> {/* Additional Charts & Widgets */} <div className="grid grid-cols-1 lg:grid-cols-3 gap-3 md:gap-6"> <div className="chart-container lg:col-span-2"> <ChartCard title="Bugs Created (Last 7 days)" data={[trendData]} colors={["#6366F1"]} type="line" showGrid={true} className="shadow-sm hover-scale text-primary" />
</div>
<Card className="shadow-sm hover-scale"> <CardHeader className="py-3 md:py-4"> <CardTitle className="text-base md:text-lg font-semibold flex items-center gap-2 text-primary"> <PieChart className="h-4 w-4 md:h-5 md:w-5 text-indigo-500" /> Top Bug Handlers </CardTitle> </CardHeader> <CardContent className="py-2 md:py-3"> <div className="space-y-3 md:space-y-4 scrollable-content max-h-[250px] overflow-y-auto"> {topAssignees.length > 0 ? topAssignees.map((assignee, index) => ( <div key={assignee.id} className="flex items-center justify-between stagger-item"> <div className="flex items-center gap-2 md:gap-3"> <Avatar className="h-7 w-7 md:h-8 md:w-8 bg-indigo-100"> <AvatarFallback className="bg-indigo-500 text-white text-xs"> {assignee.initials} </AvatarFallback> </Avatar> <div> <p className="text-xs md:text-sm font-medium truncate max-w-[100px] md:max-w-[140px]">{assignee.name}</p> <p className="text-[10px] md:text-xs text-muted-foreground"> {assignee.count} {assignee.count === 1 ? "bug" : "bugs"} </p> </div> </div> <div className="flex items-center gap-2"> <div className="h-2 md:h-2.5 bg-gray-200 rounded-full w-16 md:w-20"> <div className="h-2 md:h-2.5 bg-indigo-500 rounded-full progress-bar" style={{ width: `${Math.min(100, (assignee.count / Math.max(...topAssignees.map(a => a.count))) * 100)}%` }} ></div> </div> </div> </div> )) : ( <div className="text-center py-4 text-muted-foreground text-xs md:text-sm"> <p>No assigned bugs yet</p> </div> )} </div> </CardContent> </Card>
</div>
{/* Recent Issues */} <Card className="shadow-sm hover-scale"> <CardHeader className="py-3 md:py-4"> <CardTitle className="text-base md:text-lg font-semibold flex items-center gap-2 text-primary"> <Bug className="h-4 w-4 md:h-5 md:w-5 text-indigo-500" /> Recent Issues </CardTitle> </CardHeader> <CardContent className="py-2 md:py-3"> {bugs.length > 0 ? ( <div className="space-y-3 scrollable-content max-h-[350px] overflow-y-auto"> {bugs .sort((a, b) => { const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0; const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0; return dateB - dateA; }) .slice(0, 5) .map((bug, index) => ( <div key={bug.id} className="p-2 md:p-3 rounded-lg border bg-card hover:bg-accent/10 transition-colors stagger-item"> <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2 md:gap-0"> <div className="flex flex-col"> <div className="flex flex-wrap items-center gap-1 md:gap-2"> <p className="font-medium text-sm md:text-base">{bug.title}</p> <p className="text-[10px] md:text-xs text-muted-foreground bg-muted px-1.5 md:px-2 py-0.5 rounded"> {bug.taskCode || `BUG-${bug.id}`} </p> </div> <p className="text-[10px] md:text-xs text-muted-foreground mt-1 line-clamp-2">{bug.description}</p> </div> <div className="flex flex-row md:flex-col items-start md:items-end gap-1 md:gap-2 flex-wrap"> {getStatusBadge(bug.status)} {getSeverityBadge(bug)} </div> </div> <div className="flex justify-between items-center mt-2 md:mt-3 flex-wrap gap-1"> <div className="flex items-center gap-1 md:gap-2"> {bug.assignedTo ? ( <div className="flex items-center gap-1 md:gap-2"> <Avatar className="h-5 w-5 md:h-6 md:w-6"> <AvatarFallback className="text-[10px] md:text-xs"> {(() => { const employee = employees.find(emp => emp.id === bug.assignedTo); return employee ? `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase() : "UN"; })()} </AvatarFallback> </Avatar> <span className="text-[10px] md:text-xs text-muted-foreground truncate max-w-[80px] md:max-w-none"> {(() => { const employee = employees.find(emp => emp.id === bug.assignedTo); return employee ? `${employee.firstName} ${employee.lastName}` : `User ${bug.assignedTo}`; })()} </span> </div> ) : ( <span className="text-[10px] md:text-xs text-muted-foreground">Unassigned</span> )} </div> <div className="text-[10px] md:text-xs text-muted-foreground"> {bug.createdAt ? new Date(bug.createdAt).toLocaleDateString() : "N/A"} </div> </div> </div> ))} </div> ) : ( <div className="text-center py-4 text-muted-foreground text-xs md:text-sm"> <p>No bugs available for your assigned projects</p> </div> )}
</CardContent>
</Card>
</div>
);
};
export default DashboardBugs;