import React, { useState } from 'react'; import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { Download, RefreshCw, TrendingUp, CheckCircle, Clock, AlertCircle } from 'lucide-react'; const JiraEpicDashboard = () => { const [jiraUrl, setJiraUrl] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [epicKey, setEpicKey] = useState(''); const [loading, setLoading] = useState(false); const [data, setData] = useState(null); const [error, setError] = useState(''); const fetchEpicData = async () => { setLoading(true); setError(''); try { const baseUrl = jiraUrl.replace(/\/$/, ''); const auth = btoa(`${username}:${password}`); // First, get the epic details const epicResponse = await fetch(`${baseUrl}/rest/api/3/issue/${epicKey}`, { headers: { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json' } }); if (!epicResponse.ok) { throw new Error(`Failed to fetch epic: ${epicResponse.status}`); } const epicData = await epicResponse.json(); // Then get all issues in the epic const jql = `"Epic Link" = ${epicKey} OR parent = ${epicKey}`; const searchResponse = await fetch( `${baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=1000&fields=summary,status,assignee,priority,timetracking,customfield_10016,issuetype`, { headers: { 'Authorization': `Basic ${auth}`, 'Accept': 'application/json' } } ); if (!searchResponse.ok) { throw new Error(`Failed to fetch issues: ${searchResponse.status}`); } const searchData = await searchResponse.json(); // Process the data processData(epicData, searchData.issues); } catch (err) { setError(err.message); console.error('Error fetching data:', err); } finally { setLoading(false); } }; const processData = (epic, issues) => { const statusCounts = {}; const typeCounts = {}; const assigneeCounts = {}; let totalStoryPoints = 0; let completedStoryPoints = 0; let totalOriginalEstimate = 0; let totalRemainingEstimate = 0; let totalTimeSpent = 0; issues.forEach(issue => { const status = issue.fields.status.name; const type = issue.fields.issuetype.name; const assignee = issue.fields.assignee?.displayName || 'Unassigned'; // Count by status statusCounts[status] = (statusCounts[status] || 0) + 1; // Count by type typeCounts[type] = (typeCounts[type] || 0) + 1; // Count by assignee assigneeCounts[assignee] = (assigneeCounts[assignee] || 0) + 1; // Story points (customfield_10016 is common for story points, may vary) const storyPoints = issue.fields.customfield_10016 || 0; totalStoryPoints += storyPoints; if (status === 'Done' || status === 'Closed') { completedStoryPoints += storyPoints; } // Time tracking if (issue.fields.timetracking) { totalOriginalEstimate += issue.fields.timetracking.originalEstimateSeconds || 0; totalRemainingEstimate += issue.fields.timetracking.remainingEstimateSeconds || 0; totalTimeSpent += issue.fields.timetracking.timeSpentSeconds || 0; } }); const statusData = Object.entries(statusCounts).map(([name, value]) => ({ name, value })); const typeData = Object.entries(typeCounts).map(([name, value]) => ({ name, value })); const assigneeData = Object.entries(assigneeCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([name, value]) => ({ name, value })); setData({ epicName: epic.fields.summary, epicKey: epic.key, totalIssues: issues.length, statusData, typeData, assigneeData, storyPoints: { total: totalStoryPoints, completed: completedStoryPoints, remaining: totalStoryPoints - completedStoryPoints }, timeTracking: { originalEstimate: totalOriginalEstimate / 3600, remaining: totalRemainingEstimate / 3600, spent: totalTimeSpent / 3600 }, issues: issues.map(i => ({ key: i.key, summary: i.fields.summary, status: i.fields.status.name, assignee: i.fields.assignee?.displayName || 'Unassigned', type: i.fields.issuetype.name })) }); }; const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FFC658', '#FF6B9D']; const exportToCSV = () => { if (!data) return; const csv = [ ['Key', 'Summary', 'Status', 'Assignee', 'Type'], ...data.issues.map(i => [i.key, i.summary, i.status, i.assignee, i.type]) ].map(row => row.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${data.epicKey}_export.csv`; a.click(); }; if (!data) { return (
{data.epicKey}
Total Issues
{data.totalIssues}
Story Points
{data.storyPoints.total}
{data.storyPoints.completed} completed
Completion
{completionPercentage}%
Time Remaining
{data.timeTracking.remaining.toFixed(1)}h
{data.timeTracking.spent.toFixed(1)}h spent
| Key | Summary | Status | Assignee | Type |
|---|---|---|---|---|
| {issue.key} | {issue.summary} | {issue.status} | {issue.assignee} | {issue.type} |