From 824d33f283f75b579b00c540f6a730458c715e42 Mon Sep 17 00:00:00 2001 From: Gabriel Santana de Menezes Date: Sat, 9 May 2026 20:28:22 -0400 Subject: [PATCH] chore: removing unecessary lines Co-Authored-By: Claude Sonnet 4.6 --- .../orgdashboard/MemberDetailController.java | 22 +- .../MemberDetailControllerMapper.java | 11 +- .../orgdashboard/MemberDetailResponse.java | 7 +- .../memberdetail/MemberDetailInput.java | 5 +- .../memberdetail/MemberDetailMapper.java | 9 +- .../memberdetail/MemberDetailOutput.java | 7 +- .../memberdetail/MemberDetailUseCase.java | 95 +++-- frontend/src/components/MemberDetailModal.tsx | 341 +++++++++++------- frontend/src/pages/OrgDashboardPage.tsx | 2 + frontend/src/services/orgDashboardService.ts | 18 +- 10 files changed, 338 insertions(+), 179 deletions(-) diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailController.java index f0b944d..50a5a0a 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailController.java @@ -7,10 +7,14 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import net.beetechgroup.beetask.usecase.orgdashboard.memberdetail.MemberDetailInput; import net.beetechgroup.beetask.usecase.orgdashboard.memberdetail.MemberDetailUseCase; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Path("/organizations/{id}/members/{memberId}/stats") @Produces(MediaType.APPLICATION_JSON) @Authenticated @@ -25,12 +29,26 @@ public class MemberDetailController { @GET public MemberDetailResponse getMemberStats( @PathParam("id") Long organizationId, - @PathParam("memberId") Long memberId) { + @PathParam("memberId") Long memberId, + @QueryParam("startDate") String startDateStr, + @QueryParam("endDate") String endDateStr) { String email = securityIdentity.getPrincipal().getName(); + LocalDateTime start = startDateStr != null + ? LocalDateTime.parse(startDateStr, DateTimeFormatter.ISO_DATE_TIME) + : LocalDateTime.now().minusMonths(1); + + LocalDateTime end = endDateStr != null + ? LocalDateTime.parse(endDateStr, DateTimeFormatter.ISO_DATE_TIME) + : LocalDateTime.now(); + + if (endDateStr != null && end.toLocalTime().equals(java.time.LocalTime.MIDNIGHT)) { + end = end.with(java.time.LocalTime.MAX); + } + return MemberDetailControllerMapper.toResponse( - memberDetailUseCase.execute(new MemberDetailInput(email, organizationId, memberId)) + memberDetailUseCase.execute(new MemberDetailInput(email, organizationId, memberId, start, end)) ); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailControllerMapper.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailControllerMapper.java index cffe548..9e7b553 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailControllerMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailControllerMapper.java @@ -10,9 +10,14 @@ public class MemberDetailControllerMapper { output.userName(), output.userEmail(), output.userPhoto(), - output.monthlyStats().stream() - .map(s -> new MemberDetailResponse.MonthlyStatsResponse( - s.year(), s.month(), s.finishedTasksCount(), s.totalMinutesWorked())) + output.groupedBy(), + output.periodStats().stream() + .map(s -> new MemberDetailResponse.PeriodStatsResponse( + s.year(), s.month(), s.day(), s.finishedTasksCount(), s.totalMinutesWorked())) + .toList(), + output.projectStats().stream() + .map(p -> new MemberDetailResponse.ProjectStatsResponse( + p.projectId(), p.projectName(), p.totalMinutes())) .toList() ); } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailResponse.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailResponse.java index 7509ca2..92d714f 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailResponse.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/orgdashboard/MemberDetailResponse.java @@ -7,7 +7,10 @@ public record MemberDetailResponse( String userName, String userEmail, String userPhoto, - List monthlyStats + String groupedBy, + List periodStats, + List projectStats ) { - public record MonthlyStatsResponse(int year, int month, long finishedTasksCount, long totalMinutesWorked) {} + public record PeriodStatsResponse(int year, int month, Integer day, long finishedTasksCount, long totalMinutesWorked) {} + public record ProjectStatsResponse(Long projectId, String projectName, long totalMinutes) {} } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailInput.java index 6a27260..6d99f35 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailInput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailInput.java @@ -1,4 +1,7 @@ package net.beetechgroup.beetask.usecase.orgdashboard.memberdetail; -public record MemberDetailInput(String userEmail, Long organizationId, Long memberId) { +import java.time.LocalDateTime; + +public record MemberDetailInput(String userEmail, Long organizationId, Long memberId, + LocalDateTime startDate, LocalDateTime endDate) { } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailMapper.java index ddf6ed4..522af8e 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailMapper.java @@ -6,13 +6,18 @@ import java.util.List; public class MemberDetailMapper { - public static MemberDetailOutput toOutput(UserOrganization member, List monthlyStats) { + public static MemberDetailOutput toOutput(UserOrganization member, + String groupedBy, + List periodStats, + List projectStats) { return new MemberDetailOutput( member.getUser().getId(), member.getUser().getName(), member.getUser().getEmail(), member.getUser().getPhoto(), - monthlyStats + groupedBy, + periodStats, + projectStats ); } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailOutput.java index 1e3b941..520b60b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailOutput.java @@ -7,7 +7,10 @@ public record MemberDetailOutput( String userName, String userEmail, String userPhoto, - List monthlyStats + String groupedBy, + List periodStats, + List projectStats ) { - public record MonthlyStats(int year, int month, long finishedTasksCount, long totalMinutesWorked) {} + public record PeriodStats(int year, int month, Integer day, long finishedTasksCount, long totalMinutesWorked) {} + public record ProjectStats(Long projectId, String projectName, long totalMinutes) {} } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java index 40c5419..e1ea938 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/orgdashboard/memberdetail/MemberDetailUseCase.java @@ -8,13 +8,15 @@ import net.beetechgroup.beetask.usecase.repository.TaskRepository; import net.beetechgroup.beetask.usecase.repository.UserOrganizationRepository; import java.time.Duration; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; public class MemberDetailUseCase { @@ -37,57 +39,90 @@ public class MemberDetailUseCase { .findByUserAndOrganization(input.memberId(), input.organizationId()) .orElseThrow(() -> new IllegalArgumentException("Member not found in organization")); - LocalDateTime end = LocalDateTime.now(); - LocalDateTime start = end.minusMonths(5).withDayOfMonth(1).toLocalDate().atStartOfDay(); + LocalDateTime start = input.startDate(); + LocalDateTime end = input.endDate(); + + long daySpan = ChronoUnit.DAYS.between(start.toLocalDate(), end.toLocalDate()); + boolean groupByDay = daySpan <= 30; List workedTasks = taskRepository.findTasksWorkedByUserIdInPeriod(input.memberId(), start, end); List finishedTasks = taskRepository.findTasksFinishedByUserIdInPeriod(input.memberId(), start, end); + Map projectStatsMap = new HashMap<>(); + Map minutesPerDay = new HashMap<>(); Map minutesPerMonth = new HashMap<>(); - Map finishedPerMonth = new HashMap<>(); for (Task task : workedTasks) { for (TaskHistoryItem h : task.getHistory()) { if (!isWithinPeriod(h, start, end)) continue; LocalDateTime effectiveStart = h.getStartAt().isBefore(start) ? start : h.getStartAt(); - LocalDateTime effectiveEnd = (h.getEndAt() == null || h.getEndAt().isAfter(end)) ? end : h.getEndAt(); + LocalDateTime effectiveEnd = (Objects.isNull(h.getEndAt()) || h.getEndAt().isAfter(end)) ? end : h.getEndAt(); if (effectiveStart.isAfter(effectiveEnd)) continue; - YearMonth ym = YearMonth.from(effectiveStart); long minutes = Duration.between(effectiveStart, effectiveEnd).toMinutes(); - minutesPerMonth.merge(ym, minutes, Long::sum); + + if (groupByDay) { + minutesPerDay.merge(effectiveStart.toLocalDate(), minutes, Long::sum); + } else { + minutesPerMonth.merge(YearMonth.from(effectiveStart), minutes, Long::sum); + } + + if (Objects.nonNull(task.getProject())) { + Long projectId = task.getProject().getId(); + MemberDetailOutput.ProjectStats existing = projectStatsMap.getOrDefault(projectId, + new MemberDetailOutput.ProjectStats(projectId, task.getProject().getName(), 0L)); + projectStatsMap.put(projectId, new MemberDetailOutput.ProjectStats( + projectId, existing.projectName(), existing.totalMinutes() + minutes)); + } } } - for (Task task : finishedTasks) { - if (task.getFinishedAt() == null) continue; - YearMonth ym = YearMonth.from(task.getFinishedAt()); - finishedPerMonth.merge(ym, 1L, Long::sum); + List periodStats = new ArrayList<>(); + + if (groupByDay) { + Map finishedPerDay = new HashMap<>(); + for (Task task : finishedTasks) { + if (Objects.isNull(task.getFinishedAt())) continue; + finishedPerDay.merge(task.getFinishedAt().toLocalDate(), 1L, Long::sum); + } + + LocalDate current = start.toLocalDate(); + LocalDate endDate = end.toLocalDate(); + while (!current.isAfter(endDate)) { + periodStats.add(new MemberDetailOutput.PeriodStats( + current.getYear(), current.getMonthValue(), current.getDayOfMonth(), + finishedPerDay.getOrDefault(current, 0L), + minutesPerDay.getOrDefault(current, 0L) + )); + current = current.plusDays(1); + } + } else { + Map finishedPerMonth = new HashMap<>(); + for (Task task : finishedTasks) { + if (Objects.isNull(task.getFinishedAt())) continue; + finishedPerMonth.merge(YearMonth.from(task.getFinishedAt()), 1L, Long::sum); + } + + YearMonth current = YearMonth.from(start); + YearMonth endMonth = YearMonth.from(end); + while (!current.isAfter(endMonth)) { + periodStats.add(new MemberDetailOutput.PeriodStats( + current.getYear(), current.getMonthValue(), null, + finishedPerMonth.getOrDefault(current, 0L), + minutesPerMonth.getOrDefault(current, 0L) + )); + current = current.plusMonths(1); + } } - List monthlyStats = new ArrayList<>(); - YearMonth current = YearMonth.from(start); - YearMonth endMonth = YearMonth.from(end); - while (!current.isAfter(endMonth)) { - monthlyStats.add(new MemberDetailOutput.MonthlyStats( - current.getYear(), - current.getMonthValue(), - finishedPerMonth.getOrDefault(current, 0L), - minutesPerMonth.getOrDefault(current, 0L) - )); - current = current.plusMonths(1); - } - - monthlyStats.sort(Comparator.comparingInt(MemberDetailOutput.MonthlyStats::year) - .thenComparingInt(MemberDetailOutput.MonthlyStats::month)); - - return MemberDetailMapper.toOutput(member, monthlyStats); + return MemberDetailMapper.toOutput(member, groupByDay ? "DAY" : "MONTH", periodStats, + new ArrayList<>(projectStatsMap.values())); } private boolean isWithinPeriod(TaskHistoryItem item, LocalDateTime start, LocalDateTime end) { - if (item.getStartAt() == null) return false; + if (Objects.isNull(item.getStartAt())) return false; if (item.getStartAt().isAfter(end)) return false; - if (item.getEndAt() != null && item.getEndAt().isBefore(start)) return false; + if (Objects.nonNull(item.getEndAt()) && item.getEndAt().isBefore(start)) return false; return true; } } diff --git a/frontend/src/components/MemberDetailModal.tsx b/frontend/src/components/MemberDetailModal.tsx index 9a923f7..fdb1af9 100644 --- a/frontend/src/components/MemberDetailModal.tsx +++ b/frontend/src/components/MemberDetailModal.tsx @@ -1,102 +1,83 @@ import React, { useEffect, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { X, Mail, Clock, CheckCircle2, Loader2, AlertCircle } from "lucide-react"; -import { orgDashboardService, type MemberDetailData, type MonthlyStats } from "../services/orgDashboardService"; +import { X, Mail, Clock, CheckCircle2, Loader2, AlertCircle, TrendingUp, Zap } from "lucide-react"; +import { format, differenceInDays } from "date-fns"; +import { + orgDashboardService, + type MemberDetailData, + type PeriodStats, + type MemberProjectStats, +} from "../services/orgDashboardService"; const MONTH_NAMES = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"]; - -const BAR_COLOR = "#7C3AED"; -const BAR_COLOR_2 = "#06B6D4"; - -function Avatar({ name, photo }: { name: string; photo?: string | null }) { - if (photo) { - return {name}; - } - const initials = name - .split(" ") - .slice(0, 2) - .map((n) => n[0]) - .join("") - .toUpperCase(); - return ( -
- {initials} -
- ); -} +const CHART_COLORS = ["#7C3AED", "#06B6D4", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#3B82F6", "#EC4899"]; function formatMinutes(minutes: number) { const h = Math.floor(minutes / 60); const m = minutes % 60; if (h === 0) return `${m}m`; + if (m === 0) return `${h}h`; return `${h}h ${m}m`; } +function getPeriodLabel(s: PeriodStats, groupedBy: "DAY" | "MONTH"): string { + if (groupedBy === "DAY") { + return `${String(s.day).padStart(2, "0")}/${String(s.month).padStart(2, "0")}`; + } + return `${MONTH_NAMES[s.month - 1]}/${String(s.year).slice(2)}`; +} + +function Avatar({ name, photo }: { name: string; photo?: string | null }) { + if (photo) return {name}; + const initials = name.split(" ").slice(0, 2).map((n) => n[0]).join("").toUpperCase(); + return ( +
+ {initials} +
+ ); +} + function BarChart({ data, valueKey, color, - formatValue, + groupedBy, }: { - data: MonthlyStats[]; + data: PeriodStats[]; valueKey: "finishedTasksCount" | "totalMinutesWorked"; color: string; - formatValue: (v: number) => string; + groupedBy: "DAY" | "MONTH"; }) { const values = data.map((d) => d[valueKey] as number); const maxVal = Math.max(...values, 1); - - const chartW = 320; - const chartH = 120; - const barAreaH = 90; + const chartW = 220; + const barAreaH = 80; const barCount = data.length; - const gap = 6; - const barW = barCount > 0 ? Math.floor((chartW - gap * (barCount - 1)) / barCount) : 30; + const gap = barCount > 15 ? 2 : 4; + const barW = barCount > 0 ? Math.max(2, Math.floor((chartW - gap * (barCount - 1)) / barCount)) : 10; + + const showLabel = (i: number) => { + if (groupedBy === "DAY") return barCount <= 10 || i % Math.ceil(barCount / 6) === 0; + return true; + }; return ( - + {data.map((d, i) => { const val = d[valueKey] as number; - const barH = maxVal > 0 ? Math.round((val / maxVal) * barAreaH) : 0; + const barH = maxVal > 0 ? Math.max(val > 0 ? 2 : 0, Math.round((val / maxVal) * barAreaH)) : 0; const x = i * (barW + gap); const y = barAreaH - barH; - const label = MONTH_NAMES[(d.month - 1) % 12]; + const label = showLabel(i) ? getPeriodLabel(d, groupedBy) : ""; return ( - - {val > 0 && ( - - {formatValue(val)} + + {label && ( + + {label} )} - - {label} - ); })} @@ -104,31 +85,89 @@ function BarChart({ ); } +function MiniPieChart({ data }: { data: MemberProjectStats[] }) { + const total = data.reduce((s, d) => s + d.totalMinutes, 0); + if (total === 0 || data.length === 0) { + return ( +
+ Sem dados +
+ ); + } + + const cx = 50, cy = 50, r = 38, innerR = 22; + let startAngle = -Math.PI / 2; + + const slices = data.map((d, i) => { + const fraction = d.totalMinutes / total; + const angle = fraction * 2 * Math.PI; + const endAngle = startAngle + angle; + const x1 = cx + r * Math.cos(startAngle), y1 = cy + r * Math.sin(startAngle); + const x2 = cx + r * Math.cos(endAngle), y2 = cy + r * Math.sin(endAngle); + const ix1 = cx + innerR * Math.cos(endAngle), iy1 = cy + innerR * Math.sin(endAngle); + const ix2 = cx + innerR * Math.cos(startAngle), iy2 = cy + innerR * Math.sin(startAngle); + const largeArc = angle > Math.PI ? 1 : 0; + const path = `M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} L ${ix1} ${iy1} A ${innerR} ${innerR} 0 ${largeArc} 0 ${ix2} ${iy2} Z`; + const result = { path, color: CHART_COLORS[i % CHART_COLORS.length], name: d.projectName, fraction }; + startAngle = endAngle; + return result; + }); + + return ( + + {slices.map((s, i) => ( + + ))} + + ); +} + interface MemberDetailModalProps { memberId: number | null; orgId: number; + startDate: Date; + endDate: Date; onClose: () => void; } -export const MemberDetailModal: React.FC = ({ memberId, orgId, onClose }) => { +export const MemberDetailModal: React.FC = ({ + memberId, + orgId, + startDate, + endDate, + onClose, +}) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { - if (memberId === null) { - setData(null); - return; - } + if (memberId === null) { setData(null); return; } setIsLoading(true); setError(null); setData(null); + const start = format(startDate, "yyyy-MM-dd'T'HH:mm:ss"); + const end = format(endDate, "yyyy-MM-dd'T'HH:mm:ss"); orgDashboardService - .getMemberStats(orgId, memberId) + .getMemberStats(orgId, memberId, start, end) .then(setData) .catch(() => setError("Falha ao carregar os dados do membro.")) .finally(() => setIsLoading(false)); - }, [memberId, orgId]); + }, [memberId, orgId, startDate, endDate]); + + const totalMinutes = data?.periodStats.reduce((s, p) => s + p.totalMinutesWorked, 0) ?? 0; + const totalFinished = data?.periodStats.reduce((s, p) => s + p.finishedTasksCount, 0) ?? 0; + + const daySpan = Math.max(differenceInDays(endDate, startDate), 1); + const avgPerDay = totalMinutes / daySpan; + + const bestPeriod = data?.periodStats.reduce( + (best, p) => (p.totalMinutesWorked > (best?.totalMinutesWorked ?? -1) ? p : best), + null as PeriodStats | null, + ); + const bestLabel = bestPeriod && bestPeriod.totalMinutesWorked > 0 + ? getPeriodLabel(bestPeriod, data!.groupedBy) + : "—"; return ( @@ -139,99 +178,135 @@ export const MemberDetailModal: React.FC = ({ memberId, animate={{ opacity: 1 }} exit={{ opacity: 0 }} > -
+
-
+
{isLoading ? ( -
- +
+

Carregando dados...

) : error ? ( -
- +
+

{error}

) : data ? ( -
+
{/* Header */} -
+
-

{data.userName}

-
- +

{data.userName}

+
+ {data.userEmail}
- {/* Summary chips */} + {/* 4 stat chips */} +
+
+ +
{totalFinished}
+
Concluídas
+
+
+ +
{formatMinutes(totalMinutes)}
+
Trabalhado
+
+
+ +
{formatMinutes(Math.round(avgPerDay))}
+
Média/dia
+
+
+ +
{bestLabel}
+
Melhor período
+
+
+ + {/* Bar charts side by side */}
-
- -
- {data.monthlyStats.reduce((s, m) => s + m.finishedTasksCount, 0)} -
-
- Concluídas (6m) +
+
+ + + Tarefas Concluídas {data.groupedBy === "DAY" ? "por Dia" : "por Mês"} +
+
-
- -
- {formatMinutes(data.monthlyStats.reduce((s, m) => s + m.totalMinutesWorked, 0))} -
-
- Trabalhado (6m) + +
+
+ + + Tempo Trabalhado {data.groupedBy === "DAY" ? "por Dia" : "por Mês"} +
+
- {/* Chart: finished tasks per month */} -
-
- - Tarefas Concluídas por Mês + {/* Pie chart — hours by project */} + {data.projectStats.length > 0 && ( +
+
+ Horas por Projeto +
+
+
+ +
+
+ {data.projectStats + .slice() + .sort((a, b) => b.totalMinutes - a.totalMinutes) + .map((p, i) => { + const total = data.projectStats.reduce((s, x) => s + x.totalMinutes, 0); + const pct = total > 0 ? ((p.totalMinutes / total) * 100).toFixed(0) : "0"; + return ( +
+
+ {p.projectName} + + {formatMinutes(p.totalMinutes)} ({pct}%) + +
+ ); + })} +
+
- String(v)} - /> -
- - {/* Chart: minutes worked per month */} -
-
- - Tempo Trabalhado por Mês -
- -
+ )}
) : null}
diff --git a/frontend/src/pages/OrgDashboardPage.tsx b/frontend/src/pages/OrgDashboardPage.tsx index 1ca97fb..5d69f6f 100644 --- a/frontend/src/pages/OrgDashboardPage.tsx +++ b/frontend/src/pages/OrgDashboardPage.tsx @@ -424,6 +424,8 @@ export const OrgDashboardPage: React.FC = () => { setSelectedMemberId(null)} /> )} diff --git a/frontend/src/services/orgDashboardService.ts b/frontend/src/services/orgDashboardService.ts index b950b55..68d943b 100644 --- a/frontend/src/services/orgDashboardService.ts +++ b/frontend/src/services/orgDashboardService.ts @@ -27,19 +27,28 @@ export interface OrgDashboardData { memberStats: OrgMemberStats[] } -export interface MonthlyStats { +export interface PeriodStats { year: number month: number + day: number | null finishedTasksCount: number totalMinutesWorked: number } +export interface MemberProjectStats { + projectId: number + projectName: string + totalMinutes: number +} + export interface MemberDetailData { userId: number userName: string userEmail: string userPhoto: string | null - monthlyStats: MonthlyStats[] + groupedBy: 'DAY' | 'MONTH' + periodStats: PeriodStats[] + projectStats: MemberProjectStats[] } export const orgDashboardService = { @@ -50,7 +59,8 @@ export const orgDashboardService = { return apiFetch(`/organizations/${orgId}/dashboard?${params.toString()}`) }, - async getMemberStats(orgId: number, memberId: number): Promise { - return apiFetch(`/organizations/${orgId}/members/${memberId}/stats`) + async getMemberStats(orgId: number, memberId: number, startDate: string, endDate: string): Promise { + const params = new URLSearchParams({ startDate, endDate }) + return apiFetch(`/organizations/${orgId}/members/${memberId}/stats?${params.toString()}`) } }