diff --git a/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintCard.jsx b/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintCard.jsx
index 0fc23d1f9..a07e8741a 100644
--- a/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintCard.jsx
+++ b/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintCard.jsx
@@ -8,7 +8,7 @@ const fmt = (iso) => {
return `${d}-${m}-${y}`;
};
-export default function SprintCard({ sprint, isManager, onActivate, onClose, onSelect }) {
+export default function SprintCard({ sprint, isManager, onActivate, onClose, onReopen, onSelect }) {
return (
onSelect(sprint)}
@@ -27,13 +27,21 @@ export default function SprintCard({ sprint, isManager, onActivate, onClose, onS
{isManager && sprint.status !== 'CLOSED' && (
e.stopPropagation()}>
{sprint.status === 'PLANNING' && (
-
+ <>
+
+
+ >
)}
{sprint.status === 'ACTIVE' && (
)}
)}
+ {isManager && sprint.status === 'CLOSED' && (
+
e.stopPropagation()}>
+
+
+ )}
);
}
diff --git a/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintList.jsx b/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintList.jsx
index 77a108c64..66fad50f4 100644
--- a/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintList.jsx
+++ b/MtdrSpring/backend/src/main/frontend/src/components/sprints/SprintList.jsx
@@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import SprintCard from './SprintCard';
-export default function SprintList({ sprints, isManager, onActivate, onClose, onSelect }) {
+export default function SprintList({ sprints, isManager, onActivate, onClose, onReopen, onSelect }) {
const ref = useRef(null);
const [dragging, setDragging] = useState(false);
const [startX, setStartX] = useState(0);
@@ -43,7 +43,7 @@ export default function SprintList({ sprints, isManager, onActivate, onClose, on
>
{sprints.map((s) => (
-
+
))}
diff --git a/MtdrSpring/backend/src/main/frontend/src/pages/MyProjectsPage.jsx b/MtdrSpring/backend/src/main/frontend/src/pages/MyProjectsPage.jsx
index 8f268a19a..f139b86b5 100644
--- a/MtdrSpring/backend/src/main/frontend/src/pages/MyProjectsPage.jsx
+++ b/MtdrSpring/backend/src/main/frontend/src/pages/MyProjectsPage.jsx
@@ -7,6 +7,7 @@ import ProjectForm from '../components/projects/ProjectForm';
import Modal from '../components/common/Modal';
import Button from '../components/common/Button';
import LoadingSpinner from '../components/common/LoadingSpinner';
+import ConfirmDialog from '../components/common/ConfirmDialog';
export default function MyProjectsPage() {
const { user } = useAuth();
@@ -16,6 +17,10 @@ export default function MyProjectsPage() {
const [error, setError] = useState('');
const [showCreate, setShowCreate] = useState(false);
const [saving, setSaving] = useState(false);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleting, setDeleting] = useState(false);
+ const [editTarget, setEditTarget] = useState(null);
+ const [editSaving, setEditSaving] = useState(false);
const load = () => {
setLoading(true);
@@ -40,6 +45,36 @@ export default function MyProjectsPage() {
}
};
+ const handleEditProject = async (form) => {
+ if (!editTarget) return;
+ setEditSaving(true);
+ try {
+ const updated = await projectService.update(editTarget.id, form);
+ setProjects((prev) => prev.map((p) => (p.id === editTarget.id ? updated : p)));
+ setEditTarget(null);
+ toast.success('Proyecto actualizado');
+ } catch (err) {
+ toast.error(err.message);
+ } finally {
+ setEditSaving(false);
+ }
+ };
+
+ const handleDeleteProject = async () => {
+ if (!deleteTarget) return;
+ setDeleting(true);
+ try {
+ await projectService.delete(deleteTarget.id);
+ setProjects((prev) => prev.filter((p) => p.id !== deleteTarget.id));
+ setDeleteTarget(null);
+ toast.success('Proyecto eliminado');
+ } catch (err) {
+ toast.error(err.message);
+ } finally {
+ setDeleting(false);
+ }
+ };
+
return (
@@ -54,13 +89,37 @@ export default function MyProjectsPage() {
)}
{projects.map((p) => (
-
+
))}
setShowCreate(false)} title="New project">
setShowCreate(false)} loading={saving} />
+
+
setEditTarget(null)} title="Editar proyecto">
+ setEditTarget(null)}
+ loading={editSaving}
+ />
+
+
+
setDeleteTarget(null)}
+ loading={deleting}
+ />
);
}
diff --git a/MtdrSpring/backend/src/main/frontend/src/pages/SprintsPage.jsx b/MtdrSpring/backend/src/main/frontend/src/pages/SprintsPage.jsx
index 7a2021f5c..50c68f171 100644
--- a/MtdrSpring/backend/src/main/frontend/src/pages/SprintsPage.jsx
+++ b/MtdrSpring/backend/src/main/frontend/src/pages/SprintsPage.jsx
@@ -32,6 +32,7 @@ export default function SprintsPage() {
const [taskSaving, setTaskSaving] = useState(false);
const [confirmCloseSprint, setConfirmCloseSprint] = useState(null);
+ const [confirmReopenSprint, setConfirmReopenSprint] = useState(null);
const [confirmDeleteTask, setConfirmDeleteTask] = useState(null);
const [actionLoading, setActionLoading] = useState(false);
@@ -95,6 +96,20 @@ export default function SprintsPage() {
finally { setActionLoading(false); }
};
+ const handleReopen = (sprint) => {
+ setConfirmReopenSprint(sprint);
+ };
+
+ const doReopenSprint = async () => {
+ setActionLoading(true);
+ try {
+ await sprintService.reopen(project.id, confirmReopenSprint.id);
+ setConfirmReopenSprint(null);
+ load();
+ } catch (err) { toast.error(err.message); }
+ finally { setActionLoading(false); }
+ };
+
const handleStatusChange = async (task, status) => {
try {
const updated = await taskService.changeStatus(project.id, task.id, status);
@@ -142,7 +157,7 @@ export default function SprintsPage() {
const closedSprints = sprints.filter((s) => s.status === 'CLOSED').sort(byDate);
- const listProps = { isManager, onActivate: handleActivate, onClose: handleClose, onSelect: loadSprintTasks };
+ const listProps = { isManager, onActivate: handleActivate, onClose: handleClose, onReopen: handleReopen, onSelect: loadSprintTasks };
return (
@@ -243,12 +258,23 @@ export default function SprintsPage() {
onClose={() => setConfirmCloseSprint(null)}
onConfirm={doCloseSprint}
title="Close sprint"
- message={`Close "${confirmCloseSprint?.sprintName}"? All tasks will be frozen and this cannot be undone.`}
+ message={`Close "${confirmCloseSprint?.sprintName}"? All tasks will be frozen. You can reopen it later if needed.`}
confirmLabel="Close sprint"
variant="warning"
loading={actionLoading}
/>
+
setConfirmReopenSprint(null)}
+ onConfirm={doReopenSprint}
+ title="Reopen sprint"
+ message={`"${confirmReopenSprint?.sprintName}" will return to PLANNING. Its tasks will become editable again.`}
+ confirmLabel="Reopen"
+ variant="warning"
+ loading={actionLoading}
+ />
+
setConfirmDeleteTask(null)}
diff --git a/MtdrSpring/backend/src/main/frontend/src/services/sprintService.js b/MtdrSpring/backend/src/main/frontend/src/services/sprintService.js
index a3d2db1d0..3ad89a88f 100644
--- a/MtdrSpring/backend/src/main/frontend/src/services/sprintService.js
+++ b/MtdrSpring/backend/src/main/frontend/src/services/sprintService.js
@@ -19,4 +19,7 @@ export const sprintService = {
close: (projectId, sprintId) =>
apiFetch(`/projects/${projectId}/sprints/${sprintId}/close`, { method: 'POST' }),
+
+ reopen: (projectId, sprintId) =>
+ apiFetch(`/projects/${projectId}/sprints/${sprintId}/reopen`, { method: 'POST' }),
};
diff --git a/MtdrSpring/backend/src/main/frontend/src/setupTests.js b/MtdrSpring/backend/src/main/frontend/src/setupTests.js
new file mode 100644
index 000000000..7b0828bfa
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/setupTests.js
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/ProjectService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/ProjectService.java
index 9060c8d3b..53a8c1e49 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/ProjectService.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/ProjectService.java
@@ -15,6 +15,7 @@
import com.springboot.MyTodoList.sprint.SprintRepository;
import com.springboot.MyTodoList.task.Task;
import com.springboot.MyTodoList.task.TaskRepository;
+import com.springboot.MyTodoList.task.activity.TaskActivityRepository;
import com.springboot.MyTodoList.user.User;
import com.springboot.MyTodoList.user.UserService;
import lombok.RequiredArgsConstructor;
@@ -32,6 +33,7 @@ public class ProjectService {
private final ProjectMemberRepository memberRepository;
private final SprintRepository sprintRepository;
private final TaskRepository taskRepository;
+ private final TaskActivityRepository activityRepository;
private final UserService userService;
private final AuditLogService auditLogService;
private final ProjectMapper projectMapper;
@@ -85,6 +87,10 @@ public void deleteProject(Long userId, Long projectId) {
Project project = findProject(projectId);
requireManager(userId, project);
auditLogService.log(actor, EntityType.PROJECT, project.getId(), AuditAction.DELETE, project, null);
+ activityRepository.deleteByTask_Project(project);
+ taskRepository.deleteByProject(project);
+ sprintRepository.deleteByProject(project);
+ memberRepository.deleteByProject(project);
projectRepository.delete(project);
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/member/ProjectMemberRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/member/ProjectMemberRepository.java
index a4f547396..87adab397 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/member/ProjectMemberRepository.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/member/ProjectMemberRepository.java
@@ -15,4 +15,5 @@ public interface ProjectMemberRepository extends JpaRepository findAllByProject(Project project, Pageable pageable);
List findAllByProject(Project project);
void deleteByProjectAndEmployee(Project project, User employee);
+ void deleteByProject(Project project);
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintController.java
index 866adc4b4..8441aac1d 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintController.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintController.java
@@ -71,6 +71,14 @@ public ResponseEntity close(@PathVariable Long projectId,
return ResponseEntity.ok(sprintService.closeSprint(uid(auth), projectId, sprintId));
}
+ @PostMapping("/{sprintId}/reopen")
+ @Operation(summary = "Reopen a closed sprint (back to PLANNING)")
+ public ResponseEntity reopen(@PathVariable Long projectId,
+ @PathVariable Long sprintId,
+ Authentication auth) {
+ return ResponseEntity.ok(sprintService.reopenSprint(uid(auth), projectId, sprintId));
+ }
+
private Long uid(Authentication auth) {
return Long.parseLong(auth.getName());
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintRepository.java
index 4dbc44f22..3809088fd 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintRepository.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintRepository.java
@@ -12,4 +12,5 @@ public interface SprintRepository extends JpaRepository {
Page findByProject(Project project, Pageable pageable);
List findByProjectAndStatus(Project project, SprintStatus status);
boolean existsByProjectAndStatus(Project project, SprintStatus status);
+ void deleteByProject(Project project);
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintService.java
index 3db7386e1..5693ee372 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintService.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/sprint/SprintService.java
@@ -109,9 +109,26 @@ public SprintResponse closeSprint(Long userId, Long projectId, Long sprintId) {
if (sprint.getStatus() == SprintStatus.CLOSED) {
throw new ConflictException("Sprint is already closed");
}
+ Sprint old = cloneForAudit(sprint);
sprint.setStatus(SprintStatus.CLOSED);
sprintRepository.save(sprint);
- auditLogService.log(actor, EntityType.SPRINT, sprint.getId(), AuditAction.UPDATE, null, sprint);
+ auditLogService.log(actor, EntityType.SPRINT, sprint.getId(), AuditAction.UPDATE, old, sprint);
+ return sprintMapper.toResponse(sprint);
+ }
+
+ @Transactional
+ public SprintResponse reopenSprint(Long userId, Long projectId, Long sprintId) {
+ User actor = userService.findActiveUserById(userId);
+ Project project = projectService.findProject(projectId);
+ projectService.requireManager(userId, project);
+ Sprint sprint = findSprint(sprintId, project);
+ if (sprint.getStatus() != SprintStatus.CLOSED) {
+ throw new ConflictException("Only a CLOSED sprint can be reopened");
+ }
+ Sprint old = cloneForAudit(sprint);
+ sprint.setStatus(SprintStatus.PLANNING);
+ sprintRepository.save(sprint);
+ auditLogService.log(actor, EntityType.SPRINT, sprint.getId(), AuditAction.UPDATE, old, sprint);
return sprintMapper.toResponse(sprint);
}
diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRepository.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRepository.java
index b3207daa7..eba2da636 100644
--- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRepository.java
+++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRepository.java
@@ -76,6 +76,8 @@ SELECT t.assignedTo.id, t.assignedTo.fullName, SUM(t.storyPoints), SUM(t.actualH
""")
List