CCCPaste

550934351f72e

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 (
      

Jira Epic Dashboard

setJiraUrl(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="https://jira.yourcompany.com" />
setUsername(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="DOMAIN\username" />
setPassword(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="Your Windows password" />
setEpicKey(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="PROJ-123" />
{error && (
{error}
)}
); } const completionPercentage = data.storyPoints.total > 0 ? Math.round((data.storyPoints.completed / data.storyPoints.total) * 100) : 0; return (

{data.epicName}

{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

Issues by Status

`${name}: ${value}`} outerRadius={80} fill="#8884d8" dataKey="value" > {data.statusData.map((entry, index) => ( ))}

Issues by Type

Top Assignees

All Issues

{data.issues.map((issue, index) => ( ))}
Key Summary Status Assignee Type
{issue.key} {issue.summary} {issue.status} {issue.assignee} {issue.type}
); }; export default JiraEpicDashboard;