diff --git a/frontend/src/components/DashboardCharts.tsx b/frontend/src/components/DashboardCharts.tsx
new file mode 100644
index 0000000..935df49
--- /dev/null
+++ b/frontend/src/components/DashboardCharts.tsx
@@ -0,0 +1,193 @@
+import React, { useRef, useState } from "react";
+
+export const MONTH_NAMES = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
+export const CHART_COLORS = ["#7C3AED", "#06B6D4", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#3B82F6", "#EC4899"];
+
+export interface BarDataPoint {
+ year: number;
+ month: number;
+ day: number | null;
+ finishedTasksCount: number;
+ totalMinutesWorked: number;
+}
+
+export interface ProjectSlice {
+ projectName: string;
+ totalMinutes: number;
+}
+
+export function chartFormatMinutes(minutes: number): string {
+ 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`;
+}
+
+export function getPeriodLabel(s: BarDataPoint, 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)}`;
+}
+
+export interface TooltipState { x: number; y: number; content: string }
+
+export function ChartTooltip({ tooltip }: { tooltip: TooltipState }) {
+ const above = tooltip.y > 40;
+ return (
+
+ {tooltip.content}
+
+ );
+}
+
+export function BarChart({
+ data,
+ valueKey,
+ color,
+ groupedBy,
+ formatValue,
+}: {
+ data: BarDataPoint[];
+ valueKey: "finishedTasksCount" | "totalMinutesWorked";
+ color: string;
+ groupedBy: "DAY" | "MONTH";
+ formatValue: (v: number) => string;
+}) {
+ const [tooltip, setTooltip] = useState(null);
+ const svgRef = useRef(null);
+
+ const handleMouseMove = (e: React.MouseEvent, content: string) => {
+ const rect = svgRef.current?.getBoundingClientRect();
+ if (!rect) return;
+ setTooltip({ x: e.clientX - rect.left, y: e.clientY - rect.top, content });
+ };
+
+ const values = data.map((d) => d[valueKey] as number);
+ const maxVal = Math.max(...values, 1);
+ const chartW = 220;
+ const barAreaH = 80;
+ const barCount = data.length;
+ 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 (
+
+
+ {tooltip && }
+
+ );
+}
+
+export function ProjectMiniPieChart({ data }: { data: ProjectSlice[] }) {
+ const [tooltip, setTooltip] = useState(null);
+ const svgRef = useRef(null);
+
+ 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 pct = (fraction * 100).toFixed(0);
+ const result = { path, color: CHART_COLORS[i % CHART_COLORS.length], label: `${d.projectName}: ${chartFormatMinutes(d.totalMinutes)} (${pct}%)` };
+ startAngle = endAngle;
+ return result;
+ });
+
+ const handleMouseMove = (e: React.MouseEvent, content: string) => {
+ const rect = svgRef.current?.getBoundingClientRect();
+ if (!rect) return;
+ setTooltip({ x: e.clientX - rect.left, y: e.clientY - rect.top, content });
+ };
+
+ return (
+
+
+ {tooltip &&
}
+
+ );
+}
diff --git a/frontend/src/components/MemberDetailModal.tsx b/frontend/src/components/MemberDetailModal.tsx
index 6907b52..f91ddb8 100644
--- a/frontend/src/components/MemberDetailModal.tsx
+++ b/frontend/src/components/MemberDetailModal.tsx
@@ -6,46 +6,18 @@ import {
orgDashboardService,
type MemberDetailData,
type PeriodStats,
- type MemberProjectStats,
type MemberCategoryStats,
} from "../services/orgDashboardService";
import { CategoryIcon } from "./CategoryIcon";
-
-const MONTH_NAMES = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
-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)}`;
-}
-
-interface TooltipState { x: number; y: number; content: string }
-
-function ChartTooltip({ tooltip }: { tooltip: TooltipState }) {
- const above = tooltip.y > 40;
- return (
-
- {tooltip.content}
-
- );
-}
+import {
+ CHART_COLORS,
+ BarChart,
+ ProjectMiniPieChart,
+ ChartTooltip,
+ chartFormatMinutes,
+ getPeriodLabel,
+} from "./DashboardCharts";
+import type { TooltipState } from "./DashboardCharts";
function Avatar({ name, photo }: { name: string; photo?: string | null }) {
if (photo) return
;
@@ -57,105 +29,6 @@ function Avatar({ name, photo }: { name: string; photo?: string | null }) {
);
}
-function BarChart({
- data,
- valueKey,
- color,
- groupedBy,
- formatValue,
-}: {
- data: PeriodStats[];
- valueKey: "finishedTasksCount" | "totalMinutesWorked";
- color: string;
- groupedBy: "DAY" | "MONTH";
- formatValue: (v: number) => string;
-}) {
- const [tooltip, setTooltip] = useState(null);
- const svgRef = useRef(null);
-
- const handleMouseMove = (e: React.MouseEvent, content: string) => {
- const rect = svgRef.current?.getBoundingClientRect();
- if (!rect) return;
- setTooltip({ x: e.clientX - rect.left, y: e.clientY - rect.top, content });
- };
-
- const values = data.map((d) => d[valueKey] as number);
- const maxVal = Math.max(...values, 1);
- const chartW = 220;
- const barAreaH = 80;
- const barCount = data.length;
- 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 (
-
-
- {tooltip && }
-
- );
-}
function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
const [tooltip, setTooltip] = useState(null);
@@ -184,7 +57,7 @@ function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
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 pct = (fraction * 100).toFixed(0);
- const result = { path, color: d.color, label: `${d.categoryName}: ${formatMinutes(d.totalMinutes)} (${pct}%)` };
+ const result = { path, color: d.color, label: `${d.categoryName}: ${chartFormatMinutes(d.totalMinutes)} (${pct}%)` };
startAngle = endAngle;
return result;
});
@@ -220,68 +93,6 @@ function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
);
}
-function MiniPieChart({ data }: { data: MemberProjectStats[] }) {
- const [tooltip, setTooltip] = useState(null);
- const svgRef = useRef(null);
-
- 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 pct = (fraction * 100).toFixed(0);
- const result = { path, color: CHART_COLORS[i % CHART_COLORS.length], label: `${d.projectName}: ${formatMinutes(d.totalMinutes)} (${pct}%)` };
- startAngle = endAngle;
- return result;
- });
-
- const handleMouseMove = (e: React.MouseEvent, content: string) => {
- const rect = svgRef.current?.getBoundingClientRect();
- if (!rect) return;
- setTooltip({ x: e.clientX - rect.left, y: e.clientY - rect.top, content });
- };
-
- return (
-
-
- {tooltip &&
}
-
- );
-}
interface MemberDetailModalProps {
memberId: number | null;
@@ -389,12 +200,12 @@ export const MemberDetailModal: React.FC = ({
-
{formatMinutes(totalMinutes)}
+
{chartFormatMinutes(totalMinutes)}
Trabalhado
-
{formatMinutes(Math.round(avgPerDay))}
+
{chartFormatMinutes(Math.round(avgPerDay))}
Média/dia
@@ -434,7 +245,7 @@ export const MemberDetailModal: React.FC = ({
valueKey="totalMinutesWorked"
color="#06B6D4"
groupedBy={data.groupedBy}
- formatValue={formatMinutes}
+ formatValue={chartFormatMinutes}
/>
@@ -466,7 +277,7 @@ export const MemberDetailModal: React.FC = ({
{c.categoryName}
- {formatMinutes(c.totalMinutes)} ({pct}%)
+ {chartFormatMinutes(c.totalMinutes)} ({pct}%)
);
@@ -484,7 +295,7 @@ export const MemberDetailModal: React.FC = ({
{data.projectStats
@@ -498,7 +309,7 @@ export const MemberDetailModal: React.FC
= ({
{p.projectName}
- {formatMinutes(p.totalMinutes)} ({pct}%)
+ {chartFormatMinutes(p.totalMinutes)} ({pct}%)
);