Files
syndarix/frontend/src/components/projects/SprintProgress.tsx
Felipe Cardoso a4c91cb8c3 refactor(frontend): clean up code by consolidating multi-line JSX into single lines where feasible
- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits.
- Improved consistency in component imports by grouping and consolidating them.
- No functional changes, purely restructuring for clarity and maintainability.
2026-01-01 11:46:57 +01:00

239 lines
7.5 KiB
TypeScript

/**
* Sprint Progress Component
*
* Displays sprint overview with progress bar, issue stats, and burndown chart.
*/
'use client';
import { TrendingUp, Calendar } from 'lucide-react';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { ProgressBar } from './ProgressBar';
import { BurndownChart } from './BurndownChart';
import type { Sprint, BurndownDataPoint } from './types';
// ============================================================================
// Types
// ============================================================================
interface SprintProgressProps {
/** Current sprint data */
sprint: Sprint | null;
/** Burndown chart data */
burndownData?: BurndownDataPoint[];
/** List of available sprints for selector */
availableSprints?: { id: string; name: string }[];
/** Currently selected sprint ID */
selectedSprintId?: string;
/** Callback when sprint selection changes */
onSprintChange?: (sprintId: string) => void;
/** Whether data is loading */
isLoading?: boolean;
/** Additional CSS classes */
className?: string;
}
// ============================================================================
// Helper Functions
// ============================================================================
function formatSprintDates(startDate?: string, endDate?: string): string {
if (!startDate || !endDate) return 'Dates not set';
try {
const start = format(new Date(startDate), 'MMM d');
const end = format(new Date(endDate), 'MMM d, yyyy');
return `${start} - ${end}`;
} catch {
return 'Invalid dates';
}
}
function calculateProgress(sprint: Sprint): number {
if (sprint.total_issues === 0) return 0;
return Math.round((sprint.completed_issues / sprint.total_issues) * 100);
}
// ============================================================================
// Subcomponents
// ============================================================================
interface StatCardProps {
value: number;
label: string;
colorClass: string;
}
function StatCard({ value, label, colorClass }: StatCardProps) {
return (
<div className="rounded-lg border p-3 text-center">
<div className={cn('text-2xl font-bold', colorClass)}>{value}</div>
<div className="text-xs text-muted-foreground">{label}</div>
</div>
);
}
function SprintProgressSkeleton() {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-56" />
</div>
<Skeleton className="h-9 w-32" />
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Progress bar skeleton */}
<div className="space-y-2">
<div className="flex justify-between">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-12" />
</div>
<Skeleton className="h-2 w-full rounded-full" />
</div>
{/* Stats grid skeleton */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-lg border p-3 text-center">
<Skeleton className="mx-auto h-8 w-8" />
<Skeleton className="mx-auto mt-1 h-3 w-16" />
</div>
))}
</div>
{/* Burndown skeleton */}
<div className="space-y-2">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-32 w-full" />
</div>
</div>
</CardContent>
</Card>
);
}
// ============================================================================
// Main Component
// ============================================================================
export function SprintProgress({
sprint,
burndownData = [],
availableSprints = [],
selectedSprintId,
onSprintChange,
isLoading = false,
className,
}: SprintProgressProps) {
if (isLoading) {
return <SprintProgressSkeleton />;
}
if (!sprint) {
return (
<Card className={className} data-testid="sprint-progress">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" aria-hidden="true" />
Sprint Overview
</CardTitle>
<CardDescription>No active sprint</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center py-8 text-center text-muted-foreground">
<Calendar className="mb-2 h-8 w-8" aria-hidden="true" />
<p className="text-sm">No sprint is currently active</p>
<p className="mt-1 text-xs">Create a sprint to track progress</p>
</div>
</CardContent>
</Card>
);
}
const progress = calculateProgress(sprint);
const dateRange = formatSprintDates(sprint.start_date, sprint.end_date);
return (
<Card className={className} data-testid="sprint-progress">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" aria-hidden="true" />
Sprint Overview
</CardTitle>
<CardDescription>
{sprint.name} ({dateRange})
</CardDescription>
</div>
{availableSprints.length > 1 && onSprintChange && (
<Select value={selectedSprintId || sprint.id} onValueChange={onSprintChange}>
<SelectTrigger className="w-32" aria-label="Select sprint">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableSprints.map((s) => (
<SelectItem key={s.id} value={s.id}>
{s.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Sprint Progress */}
<ProgressBar
value={progress}
showLabel
aria-label={`Sprint progress: ${progress}% complete`}
/>
{/* Issue Stats Grid */}
<div
className="grid grid-cols-2 gap-4 sm:grid-cols-4"
role="list"
aria-label="Sprint issue statistics"
>
<StatCard
value={sprint.completed_issues}
label="Completed"
colorClass="text-green-600"
/>
<StatCard
value={sprint.in_progress_issues}
label="In Progress"
colorClass="text-blue-600"
/>
<StatCard value={sprint.blocked_issues} label="Blocked" colorClass="text-red-600" />
<StatCard value={sprint.todo_issues} label="To Do" colorClass="text-gray-600" />
</div>
{/* Burndown Chart */}
<div>
<h4 className="mb-2 text-sm font-medium">Burndown Chart</h4>
<BurndownChart data={burndownData} height={120} />
</div>
</div>
</CardContent>
</Card>
);
}