Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const CustomLegend = ({ payload }) => (
</div>
);

export default function EfficiencyChart({ sprintId, devId }) {
export default function EfficiencyChart({ sprintId }) {
const { project } = useProject();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
Expand All @@ -54,19 +54,18 @@ export default function EfficiencyChart({ sprintId, devId }) {
if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-oracle">{error}</p>;
if (!data) return <p className="text-sm text-gray-400">Select a sprint to view data.</p>;
const visibleMembers = (data.members || []).filter((m) => !devId || m.userId === devId);
if (!visibleMembers.length) return <p className="text-sm text-gray-400">No completed tasks in this sprint.</p>;
if (!data.members?.length) return <p className="text-sm text-gray-400">No completed tasks in this sprint.</p>;

const handleExport = () => {
const rows = visibleMembers.map((m) => ({
const rows = data.members.map((m) => ({
Miembro: m.fullName,
'SP Completados': m.spCompleted,
'Horas Reales': parseFloat(m.actualHours.toFixed(1)),
}));
exportCsv(rows, `efficiency-sprint-${sprintId}`);
};

const chartData = visibleMembers.map((m) => ({
const chartData = data.members.map((m) => ({
name: m.fullName.split(' ')[0],
'SP done': m.spCompleted,
'Hours': parseFloat(m.actualHours.toFixed(1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CustomLegend = ({ payload }) => (
</div>
);

export default function HoursByDeveloperChart({ sprints, selectedSprintId, devId }) {
export default function HoursByDeveloperChart({ sprints, selectedSprintId }) {
const { project } = useProject();
const [chartData, setChartData] = useState([]);
const [members, setMembers] = useState([]);
Expand Down Expand Up @@ -78,11 +78,10 @@ export default function HoursByDeveloperChart({ sprints, selectedSprintId, devId
});
});

let candidateIds = Object.entries(totals)
const top8ids = 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);

const data = selected.map((s, i) => {
const row = { sprint: s.sprintName };
Expand All @@ -99,7 +98,7 @@ export default function HoursByDeveloperChart({ sprints, selectedSprintId, devId
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [project.id, sprints, selectedSprintId, devId]);
}, [project.id, sprints, selectedSprintId]);

if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-oracle">{error}</p>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function initials(name) {
return name?.split(' ').map((n) => n[0]).slice(0, 2).join('') ?? '?';
}

export default function HoursPerMember({ sprintId, devId }) {
export default function HoursPerMember({ sprintId }) {
const { project } = useProject();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
Expand All @@ -30,8 +30,7 @@ export default function HoursPerMember({ sprintId, devId }) {
if (!data) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Select a sprint to view data.</p>;
if (!data.members?.length) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>No hours logged yet.</p>;

const filtered = devId ? data.members.filter((m) => m.userId === devId) : data.members;
const sorted = [...filtered].sort((a, b) => b.actualHours - a.actualHours);
const sorted = [...data.members].sort((a, b) => b.actualHours - a.actualHours);
const max = sorted[0]?.actualHours || 1;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function MiniKpi({ label, value, sub }) {
);
}

export default function KpiDevStats({ sprintId, devId }) {
export default function KpiDevStats({ sprintId }) {
const { project } = useProject();
const [effData, setEffData] = useState(null);
const [wkData, setWkData] = useState(null);
Expand Down Expand Up @@ -51,8 +51,8 @@ export default function KpiDevStats({ sprintId, devId }) {
}
if (error) return <p className="text-xs text-red-500">{error}</p>;

const effMembers = (effData?.members || []).filter((m) => !devId || m.userId === devId);
const wkMembers = (wkData || []).filter((m) => !devId || m.userId === devId);
const effMembers = effData?.members || [];
const wkMembers = wkData || [];

const hoursArr = effMembers.map((m) => m.actualHours || 0);
const tasksArr = wkMembers.map((m) => m.taskCounts?.DONE || 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CustomLegend = ({ payload }) => (
</div>
);

export default function TasksByDeveloperChart({ sprints, selectedSprintId, devId }) {
export default function TasksByDeveloperChart({ sprints, selectedSprintId }) {
const { project } = useProject();
const [chartData, setChartData] = useState([]);
const [members, setMembers] = useState([]);
Expand Down Expand Up @@ -78,11 +78,10 @@ export default function TasksByDeveloperChart({ sprints, selectedSprintId, devId
});
});

let candidateIds = Object.entries(totals)
const top8ids = 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);

const data = selected.map((s, i) => {
const row = { sprint: s.sprintName };
Expand All @@ -99,7 +98,7 @@ export default function TasksByDeveloperChart({ sprints, selectedSprintId, devId
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [project.id, sprints, selectedSprintId, devId]);
}, [project.id, sprints, selectedSprintId]);

if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-oracle">{error}</p>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const STATUSES = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];

function Skeleton() { return <div className="animate-pulse h-40 rounded-lg" style={{ background: 'var(--bg-card-alt)' }} />; }

export default function WorkloadTable({ sprintId, devId }) {
export default function WorkloadTable({ sprintId }) {
const { project } = useProject();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
Expand All @@ -26,8 +26,7 @@ export default function WorkloadTable({ sprintId, devId }) {

if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-red-500">{error}</p>;
const visibleData = data.filter((m) => !devId || m.userId === devId);
if (!visibleData.length) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Select a sprint to view data.</p>;
if (!data.length) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Select a sprint to view data.</p>;

const getValue = (member, status) =>
mode === 'tasks'
Expand All @@ -38,7 +37,7 @@ export default function WorkloadTable({ sprintId, devId }) {
STATUSES.reduce((sum, s) => sum + getValue(member, s), 0);

const handleExport = () => {
const rows = visibleData.map((m) => ({
const rows = data.map((m) => ({
Miembro: m.fullName,
'Tasks TODO': m.taskCounts?.TODO ?? 0,
'Tasks IN_PROGRESS': m.taskCounts?.IN_PROGRESS ?? 0,
Expand Down Expand Up @@ -87,8 +86,8 @@ export default function WorkloadTable({ sprintId, devId }) {
</tr>
</thead>
<tbody>
{visibleData.map((member, i) => (
<tr key={member.userId} style={{ borderBottom: i < visibleData.length - 1 ? '1px solid var(--border)' : 'none' }}>
{data.map((member, i) => (
<tr key={member.userId} style={{ borderBottom: i < data.length - 1 ? '1px solid var(--border)' : 'none' }}>
<td className="py-2.5 pr-3 font-semibold whitespace-nowrap" style={{ color: 'var(--text-primary)' }}>{member.fullName}</td>
{STATUSES.map((s) => (
<td
Expand Down
54 changes: 13 additions & 41 deletions MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useProject } from '../context/ProjectContext';
import { sprintService } from '../services/sprintService';
import { dashboardService } from '../services/dashboardService';
import FilterSelect from '../components/common/FilterSelect';
import SprintSummary from '../components/dashboard/SprintSummary';
import CompletionRate from '../components/dashboard/CompletionRate';
Expand Down Expand Up @@ -39,8 +38,6 @@ export default function DashboardPage() {

const [sprints, setSprints] = useState([]);
const [selectedSprintId, setSelectedSprintId] = useState(null);
const [developers, setDevelopers] = useState([]);
const [selectedDevId, setSelectedDevId] = useState(null);

useEffect(() => {
sprintService.list(project.id).then((data) => {
Expand All @@ -58,23 +55,6 @@ export default function DashboardPage() {
}).catch(() => {});
}, [project.id]);

// Load developer list from workload (all sprints)
useEffect(() => {
dashboardService.workload(project.id, null)
.then((members) => {
const unique = [];
const seen = new Set();
(members || []).forEach((m) => {
if (!seen.has(m.userId)) {
seen.add(m.userId);
unique.push({ id: m.userId, name: m.fullName });
}
});
setDevelopers(unique.sort((a, b) => a.name.localeCompare(b.name)));
})
.catch(() => {});
}, [project.id]);

const sid = selectedSprintId || null;

return (
Expand All @@ -87,20 +67,12 @@ export default function DashboardPage() {
</h1>
<p className="text-[12px] mt-1.5" style={{ color: 'var(--text-secondary)' }}>{project.projectName}</p>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<FilterSelect
value={selectedSprintId ? String(selectedSprintId) : ''}
onChange={(v) => setSelectedSprintId(v ? Number(v) : null)}
options={sprints.map((s) => ({ value: String(s.id), label: sprintLabel(s) }))}
placeholder="All Sprints"
/>
<FilterSelect
value={selectedDevId ? String(selectedDevId) : ''}
onChange={(v) => setSelectedDevId(v ? Number(v) : null)}
options={developers.map((d) => ({ value: String(d.id), label: d.name }))}
placeholder="All Devs"
/>
</div>
<FilterSelect
value={selectedSprintId ? String(selectedSprintId) : ''}
onChange={(v) => setSelectedSprintId(v ? Number(v) : null)}
options={sprints.map((s) => ({ value: String(s.id), label: sprintLabel(s) }))}
placeholder="All Sprints"
/>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-6 gap-4">
Expand All @@ -123,36 +95,36 @@ export default function DashboardPage() {

{/* KPI row — new: Avg/Median por dev */}
<Widget title="Developer KPIs" className="sm:col-span-2 md:col-span-6">
<KpiDevStats sprintId={sid} devId={selectedDevId} />
<KpiDevStats sprintId={sid} />
</Widget>

{/* Charts row */}
<Widget title="Velocity (SP per sprint)" className="sm:col-span-2 md:col-span-3">
<VelocityChart />
</Widget>
<Widget title="Efficiency (SP vs Hours)" className="sm:col-span-2 md:col-span-3">
<EfficiencyChart sprintId={sid} devId={selectedDevId} />
<EfficiencyChart sprintId={sid} />
</Widget>

{/* Tables row */}
<Widget title="Workload" className="sm:col-span-2 md:col-span-3">
<WorkloadTable sprintId={sid} devId={selectedDevId} />
<WorkloadTable sprintId={sid} />
</Widget>
<Widget title="Hours per Member" className="sm:col-span-2 md:col-span-3">
<HoursPerMember sprintId={sid} devId={selectedDevId} />
<HoursPerMember sprintId={sid} />
</Widget>

{/* Backlog */}
<Widget title="Backlog Summary" className="sm:col-span-2 md:col-span-6">
<BacklogSummary />
</Widget>

{/* Developer charts — filtered by sprint/dev */}
{/* Developer charts — filtered by sprint */}
<Widget title="Tasks Completed by Developer" className="sm:col-span-2 md:col-span-6">
<TasksByDeveloperChart sprints={sprints} selectedSprintId={selectedSprintId} devId={selectedDevId} />
<TasksByDeveloperChart sprints={sprints} selectedSprintId={selectedSprintId} />
</Widget>
<Widget title="Hours Worked by Developer" className="sm:col-span-2 md:col-span-6">
<HoursByDeveloperChart sprints={sprints} selectedSprintId={selectedSprintId} devId={selectedDevId} />
<HoursByDeveloperChart sprints={sprints} selectedSprintId={selectedSprintId} />
</Widget>

{/* Compare panel */}
Expand Down
Loading