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 @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -21,7 +21,7 @@ export default function CompletionRate({ sprintId }) {

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

const pct = data.completionPercentage ?? 0;
const color = pct >= 80 ? '#15803D' : pct >= 50 ? '#003865' : '#C74634';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -53,7 +53,7 @@ export default function EfficiencyChart({ sprintId }) {

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>;
if (!data) return null;
if (!data.members?.length) return <p className="text-sm text-gray-400">No completed tasks in this sprint.</p>;

const handleExport = () => {
Expand All @@ -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) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -27,7 +27,7 @@ export default function HoursPerMember({ sprintId }) {

if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-oracle">{error}</p>;
if (!data) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>Select a sprint to view data.</p>;
if (!data) return null;
if (!data.members?.length) return <p className="text-sm" style={{ color: 'var(--text-muted)' }}>No hours logged yet.</p>;

const sorted = [...data.members].sort((a, b) => b.actualHours - a.actualHours);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -24,7 +24,7 @@ export default function SprintSummary({ sprintId }) {

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

const counts = data.statusCounts || {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -26,7 +26,7 @@ export default function WorkloadTable({ sprintId }) {

if (loading) return <Skeleton />;
if (error) return <p className="text-xs text-red-500">{error}</p>;
if (!data.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)' }}>No hay datos disponibles.</p>;

const getValue = (member, status) =>
mode === 'tasks'
Expand All @@ -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 (
Expand Down
17 changes: 13 additions & 4 deletions MtdrSpring/backend/src/main/frontend/src/pages/DashboardPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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;
Expand All @@ -75,7 +77,14 @@ export default function DashboardPage() {
/>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-6 gap-4">
{!sprintsLoaded && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-6 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="animate-pulse rounded-xl h-32 md:col-span-2" style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }} />
))}
</div>
)}
{sprintsLoaded && <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-6 gap-4">

{/* Sprint Summary */}
<Widget title="Sprint Summary" className="sm:col-span-2 md:col-span-6">
Expand Down Expand Up @@ -132,7 +141,7 @@ export default function DashboardPage() {
<ComparePanel sprints={sprints} />
</Widget>

</div>
</div>}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SprintSummaryResponse> 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")
Expand All @@ -42,19 +43,21 @@ public ResponseEntity<List<VelocityResponse>> 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<EfficiencyResponse> 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<List<WorkloadResponse>> 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Integer> statusCounts = new LinkedHashMap<>();
Expand Down Expand Up @@ -65,6 +70,35 @@ public SprintSummaryResponse getSprintSummary(Long userId, Long projectId, Long
.build();
}

private SprintSummaryResponse buildSprintSummaryForProject(Project project) {
Map<String, Integer> 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<VelocityResponse> getVelocity(Long userId, Long projectId, int sprintCount) {
Project project = projectService.findProject(projectId);
Expand All @@ -85,9 +119,14 @@ public List<VelocityResponse> 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<EfficiencyResponse.MemberEfficiency> members = new ArrayList<>();
Expand All @@ -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<EfficiencyResponse.MemberEfficiency> 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<WorkloadResponse> getWorkload(Long userId, Long projectId, Long sprintId) {
public List<WorkloadResponse> 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<Long, Map<String, Long>> taskCountByUser = new HashMap<>();
Map<Long, Map<String, Long>> spByUser = new HashMap<>();

for (Object[] row : taskRepository.findWorkloadBySprint(sprint)) {
List<Object[]> 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];
Expand All @@ -127,7 +186,6 @@ public List<WorkloadResponse> getWorkload(Long userId, Long projectId, Long spri
spByUser.computeIfAbsent(memberId, k -> new HashMap<>()).put(status, sp);
}

// all project participants: manager + members
List<User> participants = new ArrayList<>();
participants.add(project.getManager());
memberRepository.findAllByProject(project).stream()
Expand Down
Loading
Loading