- Complete React + TypeScript application with Vite - Dashboard with charts and metrics visualization - Kanban board with drag-and-drop functionality - Product backlog management with user stories - Sprint planning and tracking features - Comprehensive UI component library (shadcn/ui) - Tailwind CSS styling with dark mode support - Context-based state management - Mock data for immediate testing and demonstration
231 lines
9.1 KiB
TypeScript
231 lines
9.1 KiB
TypeScript
import React from 'react';
|
|
import { useApp } from '../contexts/AppContext';
|
|
import { Card } from './ui/card';
|
|
import { Badge } from './ui/badge';
|
|
import { Progress } from './ui/progress';
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line } from 'recharts';
|
|
import { CheckCircle2, Clock, AlertCircle, TrendingUp } from 'lucide-react';
|
|
import { differenceInDays, format } from 'date-fns';
|
|
|
|
export const Dashboard: React.FC = () => {
|
|
const { tasks, userStories, activeSprint } = useApp();
|
|
|
|
if (!activeSprint) {
|
|
return (
|
|
<div className="p-8 text-center">
|
|
<h2 className="mb-2">No Active Sprint</h2>
|
|
<p className="text-muted-foreground">
|
|
Please activate a sprint from the Sprint Management page to view the dashboard.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const sprintTasks = tasks.filter(task => {
|
|
const story = userStories.find(s => s.id === task.userStoryId);
|
|
return story?.sprintId === activeSprint.id;
|
|
});
|
|
|
|
const todoTasks = sprintTasks.filter(t => t.status === 'todo').length;
|
|
const inProgressTasks = sprintTasks.filter(t => t.status === 'in-progress').length;
|
|
const blockedTasks = sprintTasks.filter(t => t.status === 'blocked').length;
|
|
const doneTasks = sprintTasks.filter(t => t.status === 'done').length;
|
|
|
|
const sprintStories = userStories.filter(s => s.sprintId === activeSprint.id);
|
|
const totalPoints = sprintStories.reduce((sum, s) => sum + s.storyPoints, 0);
|
|
const completedPoints = sprintStories
|
|
.filter(story => {
|
|
const storyTasks = tasks.filter(t => t.userStoryId === story.id);
|
|
return storyTasks.length > 0 && storyTasks.every(t => t.status === 'done');
|
|
})
|
|
.reduce((sum, s) => sum + s.storyPoints, 0);
|
|
|
|
const progressPercentage = totalPoints > 0 ? (completedPoints / totalPoints) * 100 : 0;
|
|
|
|
const daysTotal = differenceInDays(new Date(activeSprint.endDate), new Date(activeSprint.startDate));
|
|
const daysElapsed = differenceInDays(new Date(), new Date(activeSprint.startDate));
|
|
const daysRemaining = Math.max(0, differenceInDays(new Date(activeSprint.endDate), new Date()));
|
|
|
|
// Task distribution data
|
|
const taskDistribution = [
|
|
{ name: 'To Do', value: todoTasks, fill: '#94a3b8' },
|
|
{ name: 'In Progress', value: inProgressTasks, fill: '#3b82f6' },
|
|
{ name: 'Blocked', value: blockedTasks, fill: '#ef4444' },
|
|
{ name: 'Done', value: doneTasks, fill: '#10b981' }
|
|
];
|
|
|
|
// Mock burndown data (in a real app, this would be calculated from historical data)
|
|
const generateBurndownData = () => {
|
|
const data = [];
|
|
const idealBurnRate = totalPoints / daysTotal;
|
|
|
|
for (let day = 0; day <= daysTotal; day++) {
|
|
const ideal = Math.max(0, totalPoints - (idealBurnRate * day));
|
|
const actual = day <= daysElapsed
|
|
? Math.max(0, totalPoints - (completedPoints * (day / Math.max(daysElapsed, 1))))
|
|
: null;
|
|
|
|
data.push({
|
|
day: day,
|
|
ideal: Math.round(ideal),
|
|
actual: actual !== null ? Math.round(actual) : null
|
|
});
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
const burndownData = generateBurndownData();
|
|
|
|
// Find blocked tasks
|
|
const blockedTasksList = sprintTasks.filter(t => t.status === 'blocked');
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div>
|
|
<h1>Sprint Dashboard</h1>
|
|
<p className="text-muted-foreground">{activeSprint.name} - {activeSprint.goal}</p>
|
|
</div>
|
|
|
|
{/* Key Metrics */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Total Tasks</p>
|
|
<p className="text-2xl">{sprintTasks.length}</p>
|
|
</div>
|
|
<CheckCircle2 className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">In Progress</p>
|
|
<p className="text-2xl">{inProgressTasks}</p>
|
|
</div>
|
|
<Clock className="h-8 w-8 text-blue-500" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Blocked</p>
|
|
<p className="text-2xl">{blockedTasks}</p>
|
|
</div>
|
|
<AlertCircle className="h-8 w-8 text-red-500" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Days Remaining</p>
|
|
<p className="text-2xl">{daysRemaining}</p>
|
|
</div>
|
|
<TrendingUp className="h-8 w-8 text-green-500" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Sprint Progress */}
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3>Sprint Progress</h3>
|
|
<Badge>{completedPoints} / {totalPoints} points</Badge>
|
|
</div>
|
|
<Progress value={progressPercentage} className="h-3" />
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>{Math.round(progressPercentage)}% complete</span>
|
|
<span>
|
|
{format(new Date(activeSprint.startDate), 'MMM dd')} - {format(new Date(activeSprint.endDate), 'MMM dd')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Charts */}
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
{/* Task Distribution */}
|
|
<Card className="p-6">
|
|
<h3 className="mb-4">Task Distribution</h3>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<BarChart data={taskDistribution}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="name" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="value" fill="#3b82f6" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
|
|
{/* Sprint Burndown */}
|
|
<Card className="p-6">
|
|
<h3 className="mb-4">Sprint Burndown</h3>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<LineChart data={burndownData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="day" label={{ value: 'Days', position: 'insideBottom', offset: -5 }} />
|
|
<YAxis label={{ value: 'Points', angle: -90, position: 'insideLeft' }} />
|
|
<Tooltip />
|
|
<Line type="monotone" dataKey="ideal" stroke="#94a3b8" strokeDasharray="5 5" name="Ideal" />
|
|
<Line type="monotone" dataKey="actual" stroke="#3b82f6" strokeWidth={2} name="Actual" />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Blocked Items Alert */}
|
|
{blockedTasks > 0 && (
|
|
<Card className="p-6 border-red-200 bg-red-50">
|
|
<div className="flex items-start gap-4">
|
|
<AlertCircle className="h-6 w-6 text-red-500 mt-1" />
|
|
<div className="flex-1 space-y-2">
|
|
<h3 className="text-red-900">Blocked Tasks Require Attention</h3>
|
|
<p className="text-sm text-red-700">
|
|
{blockedTasks} task{blockedTasks !== 1 ? 's are' : ' is'} currently blocked and may impact sprint progress.
|
|
</p>
|
|
<div className="space-y-1 mt-3">
|
|
{blockedTasksList.map(task => (
|
|
<div key={task.id} className="text-sm text-red-800">
|
|
• {task.title}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Team Velocity Insights */}
|
|
<Card className="p-6">
|
|
<h3 className="mb-4">Team Insights</h3>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<span className="text-sm">Sprint Capacity</span>
|
|
<span className="font-medium">{activeSprint.capacity} points</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<span className="text-sm">Committed Points</span>
|
|
<span className="font-medium">{totalPoints} points</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<span className="text-sm">Completed Points</span>
|
|
<span className="font-medium">{completedPoints} points</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
|
|
<span className="text-sm">Capacity Utilization</span>
|
|
<span className="font-medium">
|
|
{activeSprint.capacity > 0 ? Math.round((totalPoints / activeSprint.capacity) * 100) : 0}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|