@@ -3,6 +3,7 @@ import { useProject } from '../context/ProjectContext';
33import { useToast } from '../context/ToastContext' ;
44import { sprintService } from '../services/sprintService' ;
55import { taskService } from '../services/taskService' ;
6+ import { exportTasksToJson } from '../services/importExportService' ;
67import SprintList from '../components/sprints/SprintList' ;
78import SprintForm from '../components/sprints/SprintForm' ;
89import TaskTable from '../components/tasks/TaskTable' ;
@@ -36,6 +37,10 @@ export default function SprintsPage() {
3637 const [ confirmDeleteTask , setConfirmDeleteTask ] = useState ( null ) ;
3738 const [ actionLoading , setActionLoading ] = useState ( false ) ;
3839
40+ const [ editSprint , setEditSprint ] = useState ( null ) ;
41+ const [ sprintSaving , setSprintSaving ] = useState ( false ) ;
42+ const [ confirmDeleteSprint , setConfirmDeleteSprint ] = useState ( null ) ;
43+
3944 const isManager = userRole === 'MANAGER' ;
4045
4146 const load = ( ) => {
@@ -110,6 +115,29 @@ export default function SprintsPage() {
110115 finally { setActionLoading ( false ) ; }
111116 } ;
112117
118+ const handleSprintUpdate = async ( form ) => {
119+ setSprintSaving ( true ) ;
120+ try {
121+ const updated = await sprintService . update ( project . id , editSprint . id , form ) ;
122+ setEditSprint ( null ) ;
123+ setSelectedSprint ( updated ) ;
124+ load ( ) ;
125+ } catch ( err ) { toast . error ( err . message ) ; }
126+ finally { setSprintSaving ( false ) ; }
127+ } ;
128+
129+ const doDeleteSprint = async ( ) => {
130+ setActionLoading ( true ) ;
131+ try {
132+ await sprintService . delete ( project . id , confirmDeleteSprint . id ) ;
133+ setConfirmDeleteSprint ( null ) ;
134+ setSelectedSprint ( null ) ;
135+ setSprintTasks ( [ ] ) ;
136+ load ( ) ;
137+ } catch ( err ) { toast . error ( err . message ) ; }
138+ finally { setActionLoading ( false ) ; }
139+ } ;
140+
113141 const handleStatusChange = async ( task , status ) => {
114142 try {
115143 const updated = await taskService . changeStatus ( project . id , task . id , status ) ;
@@ -195,16 +223,43 @@ export default function SprintsPage() {
195223 >
196224 { selectedSprint && (
197225 < div className = "space-y-4" >
198- < div className = "flex items-center gap-3" >
199- < Badge value = { selectedSprint . status } />
200- < span className = "text-xs text-gray-400" >
201- { selectedSprint . startDate } → { selectedSprint . endDate }
202- </ span >
203- { selectedSprint . goal && (
204- < span className = "text-xs text-gray-500 italic" > { selectedSprint . goal } </ span >
226+ < div className = "flex items-center justify-between gap-3" >
227+ < div className = "flex items-center gap-3 min-w-0" >
228+ < Badge value = { selectedSprint . status } />
229+ < span className = "text-xs text-gray-400 shrink-0" >
230+ { selectedSprint . startDate } → { selectedSprint . endDate }
231+ </ span >
232+ { selectedSprint . goal && (
233+ < span className = "text-xs text-gray-500 italic truncate" > { selectedSprint . goal } </ span >
234+ ) }
235+ </ div >
236+ { isManager && (
237+ < div className = "flex items-center gap-2 shrink-0" >
238+ { selectedSprint . status !== 'CLOSED' && (
239+ < Button variant = "secondary" onClick = { ( ) => setEditSprint ( selectedSprint ) } >
240+ Editar
241+ </ Button >
242+ ) }
243+ { selectedSprint . status !== 'ACTIVE' && (
244+ < Button variant = "danger" onClick = { ( ) => setConfirmDeleteSprint ( selectedSprint ) } >
245+ Eliminar
246+ </ Button >
247+ ) }
248+ </ div >
205249 ) }
206250 </ div >
207251
252+ { ! tasksLoading && sprintTasks . length > 0 && (
253+ < div className = "flex justify-end" >
254+ < Button
255+ variant = "secondary"
256+ onClick = { ( ) => exportTasksToJson ( sprintTasks , selectedSprint . sprintName ) }
257+ >
258+ Exportar tareas
259+ </ Button >
260+ </ div >
261+ ) }
262+
208263 { tasksLoading ? (
209264 < LoadingSpinner />
210265 ) : sprintTasks . length === 0 ? (
@@ -285,6 +340,29 @@ export default function SprintsPage() {
285340 variant = "danger"
286341 loading = { actionLoading }
287342 />
343+
344+ { /* Edit sprint modal */ }
345+ < Modal open = { ! ! editSprint } onClose = { ( ) => setEditSprint ( null ) } title = "Edit sprint" >
346+ { editSprint && (
347+ < SprintForm
348+ initial = { editSprint }
349+ onSubmit = { handleSprintUpdate }
350+ onCancel = { ( ) => setEditSprint ( null ) }
351+ loading = { sprintSaving }
352+ />
353+ ) }
354+ </ Modal >
355+
356+ < ConfirmDialog
357+ open = { ! ! confirmDeleteSprint }
358+ onClose = { ( ) => setConfirmDeleteSprint ( null ) }
359+ onConfirm = { doDeleteSprint }
360+ title = "Eliminar sprint"
361+ message = { `¿Eliminar "${ confirmDeleteSprint ?. sprintName } "? Las tareas del sprint se moverán al backlog. Esta acción no se puede deshacer.` }
362+ confirmLabel = "Eliminar"
363+ variant = "danger"
364+ loading = { actionLoading }
365+ />
288366 </ div >
289367 ) ;
290368}
0 commit comments