diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/AvgHoursPerSP.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/AvgHoursPerSP.jsx index ccb35a143..d1b28f76b 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/AvgHoursPerSP.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/AvgHoursPerSP.jsx @@ -11,8 +11,8 @@ export default function AvgHoursPerSP({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setData(null); return; } setLoading(true); + setError(''); dashboardService.efficiency(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/BlockedAlert.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/BlockedAlert.jsx index b8ac5789a..ca03fc976 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/BlockedAlert.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/BlockedAlert.jsx @@ -11,8 +11,8 @@ export default function BlockedAlert({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setTasks([]); return; } setLoading(true); + setError(''); dashboardService.blockedTasks(project.id, sprintId) .then(setTasks) .catch((err) => setError(err.message)) diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/CompletionRate.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/CompletionRate.jsx index 0f50ec847..3526650af 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/CompletionRate.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/CompletionRate.jsx @@ -11,8 +11,8 @@ export default function CompletionRate({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setData(null); return; } setLoading(true); + setError(''); dashboardService.sprintSummary(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) @@ -21,7 +21,7 @@ export default function CompletionRate({ sprintId }) { if (loading) return ; if (error) return

{error}

; - if (!data) return

Select a sprint to view data.

; + if (!data) return null; const pct = data.completionPercentage ?? 0; const color = pct >= 80 ? '#15803D' : pct >= 50 ? '#003865' : '#C74634'; diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx index f64423c73..0e088e33e 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx @@ -43,8 +43,8 @@ export default function EfficiencyChart({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setData(null); return; } setLoading(true); + setError(''); dashboardService.efficiency(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) @@ -53,7 +53,7 @@ export default function EfficiencyChart({ sprintId }) { if (loading) return ; if (error) return

{error}

; - if (!data) return

Select a sprint to view data.

; + if (!data) return null; if (!data.members?.length) return

No completed tasks in this sprint.

; const handleExport = () => { @@ -62,7 +62,7 @@ export default function EfficiencyChart({ sprintId }) { 'SP Completados': m.spCompleted, 'Horas Reales': parseFloat(m.actualHours.toFixed(1)), })); - exportCsv(rows, `efficiency-sprint-${sprintId}`); + exportCsv(rows, sprintId ? `efficiency-sprint-${sprintId}` : 'efficiency-all-sprints'); }; const chartData = data.members.map((m) => ({ diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx index bfd131bca..7a32c91e8 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx @@ -17,8 +17,8 @@ export default function HoursPerMember({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setData(null); return; } setLoading(true); + setError(''); dashboardService.efficiency(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) @@ -27,7 +27,7 @@ export default function HoursPerMember({ sprintId }) { if (loading) return ; if (error) return

{error}

; - if (!data) return

Select a sprint to view data.

; + if (!data) return null; if (!data.members?.length) return

No hours logged yet.

; const sorted = [...data.members].sort((a, b) => b.actualHours - a.actualHours); diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/SprintSummary.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/SprintSummary.jsx index 4a9b64c68..7e9b7194f 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/SprintSummary.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/SprintSummary.jsx @@ -14,8 +14,8 @@ export default function SprintSummary({ sprintId }) { const [error, setError] = useState(''); useEffect(() => { - if (!sprintId) { setLoading(false); setData(null); return; } setLoading(true); + setError(''); dashboardService.sprintSummary(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) @@ -24,7 +24,7 @@ export default function SprintSummary({ sprintId }) { if (loading) return ; if (error) return

{error}

; - if (!data) return

Select a sprint to view data.

; + if (!data) return null; const counts = data.statusCounts || {}; diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx index 1957de9ba..35c73cd81 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx @@ -16,8 +16,8 @@ export default function WorkloadTable({ sprintId }) { const [mode, setMode] = useState('tasks'); useEffect(() => { - if (!sprintId) { setLoading(false); setData([]); return; } setLoading(true); + setError(''); dashboardService.workload(project.id, sprintId) .then(setData) .catch((err) => setError(err.message)) @@ -26,7 +26,7 @@ export default function WorkloadTable({ sprintId }) { if (loading) return ; if (error) return

{error}

; - if (!data.length) return

Select a sprint to view data.

; + if (!data.length) return

No hay datos disponibles.

; const getValue = (member, status) => mode === 'tasks' @@ -48,7 +48,7 @@ export default function WorkloadTable({ sprintId }) { 'SP BLOCKED': m.storyPoints?.BLOCKED ?? 0, 'SP DONE': m.storyPoints?.DONE ?? 0, })); - exportCsv(rows, `workload-sprint-${sprintId}`); + exportCsv(rows, sprintId ? `workload-sprint-${sprintId}` : 'workload-all-sprints'); }; return ( diff --git a/MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx b/MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx index 8e6544873..dbfcf0b84 100644 --- a/MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx @@ -38,8 +38,10 @@ export default function DashboardPage() { const [sprints, setSprints] = useState([]); const [selectedSprintId, setSelectedSprintId] = useState(null); + const [sprintsLoaded, setSprintsLoaded] = useState(false); useEffect(() => { + setSprintsLoaded(false); sprintService.list(project.id).then((data) => { setSprints(data); const active = data.find((s) => s.status === 'ACTIVE'); @@ -50,9 +52,9 @@ export default function DashboardPage() { .filter((s) => s.status === 'CLOSED') .sort((a, b) => new Date(b.endDate) - new Date(a.endDate)); if (closed.length > 0) setSelectedSprintId(closed[0].id); - else if (data.length > 0) setSelectedSprintId(data[0].id); + else setSelectedSprintId(null); } - }).catch(() => {}); + }).catch(() => {}).finally(() => setSprintsLoaded(true)); }, [project.id]); const sid = selectedSprintId || null; @@ -75,7 +77,14 @@ export default function DashboardPage() { /> -
+ {!sprintsLoaded && ( +
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+ )} + {sprintsLoaded &&
{/* Sprint Summary */} @@ -132,7 +141,7 @@ export default function DashboardPage() { -
+
}
); } diff --git a/MtdrSpring/backend/src/main/frontend/src/services/dashboardService.js b/MtdrSpring/backend/src/main/frontend/src/services/dashboardService.js index 7a88d5959..98f278fce 100644 --- a/MtdrSpring/backend/src/main/frontend/src/services/dashboardService.js +++ b/MtdrSpring/backend/src/main/frontend/src/services/dashboardService.js @@ -2,21 +2,27 @@ import { apiFetch } from './api'; export const dashboardService = { sprintSummary: (projectId, sprintId) => { - const q = sprintId ? `?sprintId=${sprintId}` : ''; - return apiFetch(`/projects/${projectId}/dashboard/sprint-summary${q}`); + const params = new URLSearchParams(); + if (sprintId) params.set('sprintId', sprintId); + else params.set('allSprints', 'true'); + return apiFetch(`/projects/${projectId}/dashboard/sprint-summary?${params}`); }, velocity: (projectId, count = 5) => apiFetch(`/projects/${projectId}/dashboard/velocity?sprints=${count}`), efficiency: (projectId, sprintId) => { - const q = sprintId ? `?sprintId=${sprintId}` : ''; - return apiFetch(`/projects/${projectId}/dashboard/efficiency${q}`); + const params = new URLSearchParams(); + if (sprintId) params.set('sprintId', sprintId); + else params.set('allSprints', 'true'); + return apiFetch(`/projects/${projectId}/dashboard/efficiency?${params}`); }, workload: (projectId, sprintId) => { - const q = sprintId ? `?sprintId=${sprintId}` : ''; - return apiFetch(`/projects/${projectId}/dashboard/workload${q}`); + const params = new URLSearchParams(); + if (sprintId) params.set('sprintId', sprintId); + else params.set('allSprints', 'true'); + return apiFetch(`/projects/${projectId}/dashboard/workload?${params}`); }, backlog: (projectId) => diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java index fb5a8d0ea..013666a8b 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java @@ -24,11 +24,12 @@ public class DashboardController { private final DashboardService dashboardService; @GetMapping("/sprint-summary") - @Operation(summary = "Sprint summary — uses active sprint if sprintId is omitted") + @Operation(summary = "Sprint summary — uses active sprint if sprintId is omitted, all sprints if allSprints=true") public ResponseEntity sprintSummary(@PathVariable Long projectId, @RequestParam(required = false) Long sprintId, + @RequestParam(defaultValue = "false") boolean allSprints, Authentication auth) { - return ResponseEntity.ok(dashboardService.getSprintSummary(uid(auth), projectId, sprintId)); + return ResponseEntity.ok(dashboardService.getSprintSummary(uid(auth), projectId, sprintId, allSprints)); } @GetMapping("/velocity") @@ -42,19 +43,21 @@ public ResponseEntity> velocity(@PathVariable Long projec } @GetMapping("/efficiency") - @Operation(summary = "Per-member efficiency — uses active sprint if sprintId is omitted") + @Operation(summary = "Per-member efficiency — uses active sprint if sprintId is omitted, all sprints if allSprints=true") public ResponseEntity efficiency(@PathVariable Long projectId, @RequestParam(required = false) Long sprintId, + @RequestParam(defaultValue = "false") boolean allSprints, Authentication auth) { - return ResponseEntity.ok(dashboardService.getEfficiency(uid(auth), projectId, sprintId)); + return ResponseEntity.ok(dashboardService.getEfficiency(uid(auth), projectId, sprintId, allSprints)); } @GetMapping("/workload") - @Operation(summary = "Workload per member — uses active sprint if sprintId is omitted") + @Operation(summary = "Workload per member — uses active sprint if sprintId is omitted, all sprints if allSprints=true") public ResponseEntity> workload(@PathVariable Long projectId, @RequestParam(required = false) Long sprintId, + @RequestParam(defaultValue = "false") boolean allSprints, Authentication auth) { - return ResponseEntity.ok(dashboardService.getWorkload(uid(auth), projectId, sprintId)); + return ResponseEntity.ok(dashboardService.getWorkload(uid(auth), projectId, sprintId, allSprints)); } @GetMapping("/backlog") diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardService.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardService.java index 2cc6d7be8..da72d2eb9 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardService.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardService.java @@ -32,9 +32,14 @@ public class DashboardService { private final ProjectMemberRepository memberRepository; @Transactional(readOnly = true) - public SprintSummaryResponse getSprintSummary(Long userId, Long projectId, Long sprintId) { + public SprintSummaryResponse getSprintSummary(Long userId, Long projectId, Long sprintId, boolean allSprints) { Project project = projectService.findProject(projectId); projectService.requireParticipant(userId, project); + + if (allSprints) { + return buildSprintSummaryForProject(project); + } + Sprint sprint = resolveSprint(project, sprintId); Map statusCounts = new LinkedHashMap<>(); @@ -65,6 +70,35 @@ public SprintSummaryResponse getSprintSummary(Long userId, Long projectId, Long .build(); } + private SprintSummaryResponse buildSprintSummaryForProject(Project project) { + Map statusCounts = new LinkedHashMap<>(); + for (TaskStatus s : TaskStatus.values()) statusCounts.put(s.name(), 0); + + long committed = 0; + long completed = 0; + + for (Object[] row : taskRepository.findStatusCountsAndSpByProject(project)) { + TaskStatus status = (TaskStatus) row[0]; + int count = ((Long) row[1]).intValue(); + long sp = row[2] == null ? 0L : ((Number) row[2]).longValue(); + statusCounts.put(status.name(), count); + committed += sp; + if (status == TaskStatus.DONE) completed = sp; + } + + double pct = committed == 0 ? 0.0 : (double) completed / committed * 100.0; + + return SprintSummaryResponse.builder() + .sprintId(null) + .sprintName("All Sprints") + .statusCounts(statusCounts) + .spCommitted(committed) + .spCompleted(completed) + .completionPercentage(pct) + .blockedCount(statusCounts.getOrDefault("BLOCKED", 0)) + .build(); + } + @Transactional(readOnly = true) public List getVelocity(Long userId, Long projectId, int sprintCount) { Project project = projectService.findProject(projectId); @@ -85,9 +119,14 @@ public List getVelocity(Long userId, Long projectId, int sprin } @Transactional(readOnly = true) - public EfficiencyResponse getEfficiency(Long userId, Long projectId, Long sprintId) { + public EfficiencyResponse getEfficiency(Long userId, Long projectId, Long sprintId, boolean allSprints) { Project project = projectService.findProject(projectId); projectService.requireParticipant(userId, project); + + if (allSprints) { + return buildEfficiencyForProject(project); + } + Sprint sprint = resolveSprint(project, sprintId); List members = new ArrayList<>(); @@ -107,17 +146,37 @@ public EfficiencyResponse getEfficiency(Long userId, Long projectId, Long sprint return new EfficiencyResponse(sprint.getId(), sprint.getSprintName(), totalHours, totalSp, members); } + private EfficiencyResponse buildEfficiencyForProject(Project project) { + List members = new ArrayList<>(); + double totalHours = 0.0; + long totalSp = 0L; + + for (Object[] row : taskRepository.findEfficiencyByProject(project, TaskStatus.DONE)) { + Long memberId = (Long) row[0]; + String fullName = (String) row[1]; + long sp = row[2] == null ? 0L : ((Number) row[2]).longValue(); + double hours = row[3] == null ? 0.0 : ((BigDecimal) row[3]).doubleValue(); + members.add(new EfficiencyResponse.MemberEfficiency(memberId, fullName, sp, hours)); + totalSp += sp; + totalHours += hours; + } + + return new EfficiencyResponse(null, "All Sprints", totalHours, totalSp, members); + } + @Transactional(readOnly = true) - public List getWorkload(Long userId, Long projectId, Long sprintId) { + public List getWorkload(Long userId, Long projectId, Long sprintId, boolean allSprints) { Project project = projectService.findProject(projectId); projectService.requireParticipant(userId, project); - Sprint sprint = resolveSprint(project, sprintId); - // task counts and SP per user per status, from DB Map> taskCountByUser = new HashMap<>(); Map> spByUser = new HashMap<>(); - for (Object[] row : taskRepository.findWorkloadBySprint(sprint)) { + List rows = allSprints + ? taskRepository.findWorkloadByProject(project) + : taskRepository.findWorkloadBySprint(resolveSprint(project, sprintId)); + + for (Object[] row : rows) { Long memberId = (Long) row[0]; String status = ((TaskStatus) row[2]).name(); long count = (Long) row[3]; @@ -127,7 +186,6 @@ public List getWorkload(Long userId, Long projectId, Long spri spByUser.computeIfAbsent(memberId, k -> new HashMap<>()).put(status, sp); } - // all project participants: manager + members List participants = new ArrayList<>(); participants.add(project.getManager()); memberRepository.findAllByProject(project).stream() 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 517ee741a..b5610761a 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 @@ -61,6 +61,9 @@ Page findByProjectWithFilters( @Query("SELECT t.status, COUNT(t), SUM(t.storyPoints) FROM Task t WHERE t.sprint = :sprint GROUP BY t.status") List findStatusCountsAndSpBySprint(@Param("sprint") Sprint sprint); + @Query("SELECT t.status, COUNT(t), SUM(t.storyPoints) FROM Task t WHERE t.project = :project AND t.sprint IS NOT NULL GROUP BY t.status") + List findStatusCountsAndSpByProject(@Param("project") Project project); + @Query(""" SELECT t.assignedTo.id, t.assignedTo.fullName, t.status, COUNT(t), SUM(t.storyPoints) FROM Task t @@ -69,6 +72,14 @@ SELECT t.assignedTo.id, t.assignedTo.fullName, t.status, COUNT(t), SUM(t.storyPo """) List findWorkloadBySprint(@Param("sprint") Sprint sprint); + @Query(""" + SELECT t.assignedTo.id, t.assignedTo.fullName, t.status, COUNT(t), SUM(t.storyPoints) + FROM Task t + WHERE t.project = :project AND t.sprint IS NOT NULL AND t.assignedTo IS NOT NULL + GROUP BY t.assignedTo.id, t.assignedTo.fullName, t.status + """) + List findWorkloadByProject(@Param("project") Project project); + @Query(""" SELECT t.assignedTo.id, t.assignedTo.fullName, SUM(t.storyPoints), SUM(t.actualHours) FROM Task t @@ -77,6 +88,14 @@ SELECT t.assignedTo.id, t.assignedTo.fullName, SUM(t.storyPoints), SUM(t.actualH """) List findEfficiencyBySprint(@Param("sprint") Sprint sprint, @Param("status") TaskStatus status); + @Query(""" + SELECT t.assignedTo.id, t.assignedTo.fullName, SUM(t.storyPoints), SUM(t.actualHours) + FROM Task t + WHERE t.project = :project AND t.sprint IS NOT NULL AND t.status = :status AND t.assignedTo IS NOT NULL + GROUP BY t.assignedTo.id, t.assignedTo.fullName + """) + List findEfficiencyByProject(@Param("project") Project project, @Param("status") TaskStatus status); + void deleteByProject(Project project); @Modifying(clearAutomatically = true)