Spaces:
Sleeping
Sleeping
| 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; |