- Install missing UI component dependencies - Fix unused imports in React components - Remove version numbers from all import statements - Fix TypeScript errors in calendar and chart components - Build now succeeds with production bundle generated
332 lines
12 KiB
TypeScript
332 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useApp } from '../contexts/AppContext';
|
|
import { Sprint } from '../types';
|
|
import { Card } from './ui/card';
|
|
import { Badge } from './ui/badge';
|
|
import { Button } from './ui/button';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
|
import { Input } from './ui/input';
|
|
import { Textarea } from './ui/textarea';
|
|
import { Label } from './ui/label';
|
|
import { format } from 'date-fns';
|
|
import { Calendar, Plus, Edit, XCircle, PlayCircle } from 'lucide-react';
|
|
|
|
export const SprintManagement: React.FC = () => {
|
|
const { sprints, userStories, updateSprint, closeSprint, addSprint, setActiveSprint } = useApp();
|
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
const [selectedSprint, setSelectedSprint] = useState<Sprint | null>(null);
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
goal: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
capacity: 0
|
|
});
|
|
|
|
const handleEdit = (sprint: Sprint) => {
|
|
setSelectedSprint(sprint);
|
|
setFormData({
|
|
name: sprint.name,
|
|
goal: sprint.goal,
|
|
startDate: format(new Date(sprint.startDate), 'yyyy-MM-dd'),
|
|
endDate: format(new Date(sprint.endDate), 'yyyy-MM-dd'),
|
|
capacity: sprint.capacity
|
|
});
|
|
setEditDialogOpen(true);
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (selectedSprint) {
|
|
updateSprint(selectedSprint.id, {
|
|
name: formData.name,
|
|
goal: formData.goal,
|
|
startDate: new Date(formData.startDate),
|
|
endDate: new Date(formData.endDate),
|
|
capacity: formData.capacity
|
|
});
|
|
setEditDialogOpen(false);
|
|
setSelectedSprint(null);
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
addSprint({
|
|
name: formData.name,
|
|
goal: formData.goal,
|
|
startDate: new Date(formData.startDate),
|
|
endDate: new Date(formData.endDate),
|
|
capacity: formData.capacity,
|
|
status: 'planning',
|
|
userStories: []
|
|
});
|
|
setCreateDialogOpen(false);
|
|
setFormData({
|
|
name: '',
|
|
goal: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
capacity: 0
|
|
});
|
|
};
|
|
|
|
const handleCloseSprint = (sprint: Sprint) => {
|
|
if (confirm(`Are you sure you want to close ${sprint.name}? Unfinished items will be moved back to the backlog.`)) {
|
|
closeSprint(sprint.id);
|
|
}
|
|
};
|
|
|
|
const handleActivate = (sprint: Sprint) => {
|
|
updateSprint(sprint.id, { status: 'active' });
|
|
setActiveSprint(sprint);
|
|
};
|
|
|
|
const getSprintStories = (sprintId: string) => {
|
|
return userStories.filter(story => story.sprintId === sprintId);
|
|
};
|
|
|
|
const getTotalPoints = (sprintId: string) => {
|
|
return getSprintStories(sprintId).reduce((sum, story) => sum + story.storyPoints, 0);
|
|
};
|
|
|
|
const getStatusBadge = (status: Sprint['status']) => {
|
|
const variants: Record<Sprint['status'], { variant: 'default' | 'secondary' | 'outline', label: string }> = {
|
|
planning: { variant: 'outline', label: 'Planning' },
|
|
active: { variant: 'default', label: 'Active' },
|
|
closed: { variant: 'secondary', label: 'Closed' }
|
|
};
|
|
const { variant, label } = variants[status];
|
|
return <Badge variant={variant}>{label}</Badge>;
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1>Sprint Management</h1>
|
|
<p className="text-muted-foreground">
|
|
Create, edit, and manage sprints
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setCreateDialogOpen(true)}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Sprint
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{sprints.map(sprint => {
|
|
const sprintStories = getSprintStories(sprint.id);
|
|
const totalPoints = getTotalPoints(sprint.id);
|
|
|
|
return (
|
|
<Card key={sprint.id} className="p-6">
|
|
<div className="space-y-4">
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-1">
|
|
<h3>{sprint.name}</h3>
|
|
{getStatusBadge(sprint.status)}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-muted-foreground">{sprint.goal}</p>
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
<span>
|
|
{format(new Date(sprint.startDate), 'MMM dd')} - {format(new Date(sprint.endDate), 'MMM dd, yyyy')}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">Capacity:</span>
|
|
<span>{sprint.capacity} pts</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">Committed:</span>
|
|
<span>{totalPoints} pts</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground">Stories:</span>
|
|
<span>{sprintStories.length}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
{sprint.status !== 'closed' && (
|
|
<>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleEdit(sprint)}
|
|
>
|
|
<Edit className="h-4 w-4 mr-1" />
|
|
Edit
|
|
</Button>
|
|
{sprint.status === 'planning' && (
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={() => handleActivate(sprint)}
|
|
>
|
|
<PlayCircle className="h-4 w-4 mr-1" />
|
|
Activate
|
|
</Button>
|
|
)}
|
|
{sprint.status === 'active' && (
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => handleCloseSprint(sprint)}
|
|
>
|
|
<XCircle className="h-4 w-4 mr-1" />
|
|
Close
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{sprints.length === 0 && (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
No sprints yet. Create your first sprint to get started.
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit Dialog */}
|
|
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Sprint</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-name">Sprint Name</Label>
|
|
<Input
|
|
id="edit-name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-goal">Sprint Goal</Label>
|
|
<Textarea
|
|
id="edit-goal"
|
|
value={formData.goal}
|
|
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-start">Start Date</Label>
|
|
<Input
|
|
id="edit-start"
|
|
type="date"
|
|
value={formData.startDate}
|
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-end">End Date</Label>
|
|
<Input
|
|
id="edit-end"
|
|
type="date"
|
|
value={formData.endDate}
|
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-capacity">Capacity (Story Points)</Label>
|
|
<Input
|
|
id="edit-capacity"
|
|
type="number"
|
|
value={formData.capacity}
|
|
onChange={(e) => setFormData({ ...formData, capacity: parseInt(e.target.value) || 0 })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSave}>Save</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Create Dialog */}
|
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Sprint</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="create-name">Sprint Name</Label>
|
|
<Input
|
|
id="create-name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
placeholder="Sprint 1"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="create-goal">Sprint Goal</Label>
|
|
<Textarea
|
|
id="create-goal"
|
|
value={formData.goal}
|
|
onChange={(e) => setFormData({ ...formData, goal: e.target.value })}
|
|
rows={3}
|
|
placeholder="What do you want to achieve in this sprint?"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="create-start">Start Date</Label>
|
|
<Input
|
|
id="create-start"
|
|
type="date"
|
|
value={formData.startDate}
|
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="create-end">End Date</Label>
|
|
<Input
|
|
id="create-end"
|
|
type="date"
|
|
value={formData.endDate}
|
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="create-capacity">Capacity (Story Points)</Label>
|
|
<Input
|
|
id="create-capacity"
|
|
type="number"
|
|
value={formData.capacity || ''}
|
|
onChange={(e) => setFormData({ ...formData, capacity: parseInt(e.target.value) || 0 })}
|
|
placeholder="40"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleCreate}>Create</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|