Files
mock/components/Dashboard.tsx
Your Name bf1dbe39a8 Initial commit: Agile Project Manager
- 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
2025-10-24 17:58:09 -07:00

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>
);
};