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..2ae103276 100644
--- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx
+++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/EfficiencyChart.jsx
@@ -36,7 +36,7 @@ const CustomLegend = ({ payload }) => (
);
-export default function EfficiencyChart({ sprintId }) {
+export default function EfficiencyChart({ sprintId, devId }) {
const { project } = useProject();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
@@ -54,10 +54,11 @@ export default function EfficiencyChart({ sprintId }) {
if (loading) return
{error}
; if (!data) returnSelect a sprint to view data.
; - if (!data.members?.length) returnNo completed tasks in this sprint.
; + const visibleMembers = (data.members || []).filter((m) => !devId || m.userId === devId); + if (!visibleMembers.length) returnNo completed tasks in this sprint.
; const handleExport = () => { - const rows = data.members.map((m) => ({ + const rows = visibleMembers.map((m) => ({ Miembro: m.fullName, 'SP Completados': m.spCompleted, 'Horas Reales': parseFloat(m.actualHours.toFixed(1)), @@ -65,7 +66,7 @@ export default function EfficiencyChart({ sprintId }) { exportCsv(rows, `efficiency-sprint-${sprintId}`); }; - const chartData = data.members.map((m) => ({ + const chartData = visibleMembers.map((m) => ({ name: m.fullName.split(' ')[0], 'SP done': m.spCompleted, 'Hours': parseFloat(m.actualHours.toFixed(1)), diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursByDeveloperChart.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursByDeveloperChart.jsx index 0fe59f518..8ac70a616 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursByDeveloperChart.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursByDeveloperChart.jsx @@ -41,7 +41,7 @@ const CustomLegend = ({ payload }) => ( ); -export default function HoursByDeveloperChart({ sprints }) { +export default function HoursByDeveloperChart({ sprints, selectedSprintId, devId }) { const { project } = useProject(); const [chartData, setChartData] = useState([]); const [members, setMembers] = useState([]); @@ -51,19 +51,24 @@ export default function HoursByDeveloperChart({ sprints }) { useEffect(() => { if (!sprints?.length) { setLoading(false); return; } - const active = sprints.find((s) => s.status === 'ACTIVE'); - const closed = sprints - .filter((s) => s.status === 'CLOSED') - .sort((a, b) => new Date(a.endDate) - new Date(b.endDate)) - .slice(-3); - const selected = [...closed, ...(active ? [active] : [])]; + let selected; + if (selectedSprintId) { + const single = sprints.find((s) => s.id === selectedSprintId); + selected = single ? [single] : []; + } else { + const active = sprints.find((s) => s.status === 'ACTIVE'); + const closed = sprints + .filter((s) => s.status === 'CLOSED') + .sort((a, b) => new Date(a.endDate) - new Date(b.endDate)) + .slice(-3); + selected = [...closed, ...(active ? [active] : [])]; + } if (!selected.length) { setLoading(false); return; } setLoading(true); Promise.all(selected.map((s) => dashboardService.efficiency(project.id, s.id))) .then((results) => { - // Aggregate total hours per member across all sprints const totals = {}; const nameMap = {}; results.forEach((res) => { @@ -73,18 +78,17 @@ export default function HoursByDeveloperChart({ sprints }) { }); }); - // Top 8 by total hours - const top8ids = Object.entries(totals) + let candidateIds = Object.entries(totals) .sort((a, b) => b[1] - a[1]) - .slice(0, 8) .map(([id]) => Number(id)); + if (devId) candidateIds = candidateIds.filter((id) => id === devId); + const top8ids = candidateIds.slice(0, 8); - // Build recharts data const data = selected.map((s, i) => { const row = { sprint: s.sprintName }; - const members = results[i]?.members || []; + const sprintMembers = results[i]?.members || []; top8ids.forEach((uid) => { - const m = members.find((x) => x.userId === uid); + const m = sprintMembers.find((x) => x.userId === uid); row[nameMap[uid]] = m ? parseFloat(m.actualHours.toFixed(1)) : 0; }); return row; @@ -95,7 +99,7 @@ export default function HoursByDeveloperChart({ sprints }) { }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); - }, [project.id, sprints]); + }, [project.id, sprints, selectedSprintId, devId]); if (loading) return{error}
; 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..d24ad7cf1 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/HoursPerMember.jsx @@ -10,7 +10,7 @@ function initials(name) { return name?.split(' ').map((n) => n[0]).slice(0, 2).join('') ?? '?'; } -export default function HoursPerMember({ sprintId }) { +export default function HoursPerMember({ sprintId, devId }) { const { project } = useProject(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); @@ -30,7 +30,8 @@ export default function HoursPerMember({ sprintId }) { if (!data) returnSelect a sprint to view data.
; if (!data.members?.length) returnNo hours logged yet.
; - const sorted = [...data.members].sort((a, b) => b.actualHours - a.actualHours); + const filtered = devId ? data.members.filter((m) => m.userId === devId) : data.members; + const sorted = [...filtered].sort((a, b) => b.actualHours - a.actualHours); const max = sorted[0]?.actualHours || 1; return ( diff --git a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/KpiDevStats.jsx b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/KpiDevStats.jsx new file mode 100644 index 000000000..d793034c9 --- /dev/null +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/KpiDevStats.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect } from 'react'; +import { useProject } from '../../context/ProjectContext'; +import { dashboardService } from '../../services/dashboardService'; + +function Skeleton() { + return ; +} + +function median(arr) { + if (!arr.length) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; +} + +function MiniKpi({ label, value, sub }) { + return ( +{error}
; + + const effMembers = (effData?.members || []).filter((m) => !devId || m.userId === devId); + const wkMembers = (wkData || []).filter((m) => !devId || m.userId === devId); + + const hoursArr = effMembers.map((m) => m.actualHours || 0); + const tasksArr = wkMembers.map((m) => m.taskCounts?.DONE || 0); + + if (!hoursArr.length && !tasksArr.length) { + return ( ++ No hay datos para los filtros seleccionados. +
+ ); + } + + const n = Math.max(hoursArr.length, tasksArr.length); + const avgTasks = tasksArr.length ? tasksArr.reduce((s, v) => s + v, 0) / tasksArr.length : 0; + const avgHours = hoursArr.length ? hoursArr.reduce((s, v) => s + v, 0) / hoursArr.length : 0; + const medTasks = median(tasksArr); + const medHours = median(hoursArr); + + const devLabel = `${n} dev${n !== 1 ? 's' : ''}`; + + return ( +{error}
; 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..4992f9211 100644 --- a/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/components/dashboard/WorkloadTable.jsx @@ -8,7 +8,7 @@ const STATUSES = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']; function Skeleton() { return ; } -export default function WorkloadTable({ sprintId }) { +export default function WorkloadTable({ sprintId, devId }) { const { project } = useProject(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -26,7 +26,8 @@ export default function WorkloadTable({ sprintId }) { if (loading) return{error}
; - if (!data.length) returnSelect a sprint to view data.
; + const visibleData = data.filter((m) => !devId || m.userId === devId); + if (!visibleData.length) returnSelect a sprint to view data.
; const getValue = (member, status) => mode === 'tasks' @@ -37,7 +38,7 @@ export default function WorkloadTable({ sprintId }) { STATUSES.reduce((sum, s) => sum + getValue(member, s), 0); const handleExport = () => { - const rows = data.map((m) => ({ + const rows = visibleData.map((m) => ({ Miembro: m.fullName, 'Tasks TODO': m.taskCounts?.TODO ?? 0, 'Tasks IN_PROGRESS': m.taskCounts?.IN_PROGRESS ?? 0, @@ -86,8 +87,8 @@ export default function WorkloadTable({ sprintId }) { - {data.map((member, i) => ( -{project.projectName}
-