refactor: extract shared chart components to DashboardCharts.tsx
Extrai BarChart, ProjectMiniPieChart, ChartTooltip e helpers (getPeriodLabel, chartFormatMinutes) de MemberDetailModal.tsx para um arquivo compartilhado, eliminando duplicação ao reutilizá-los na dashboard do usuário. CategoryMiniPieChart permanece local por usar cores dinâmicas de categoria. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
15118177a2
commit
2ecbe090b5
2 changed files with 209 additions and 205 deletions
193
frontend/src/components/DashboardCharts.tsx
Normal file
193
frontend/src/components/DashboardCharts.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
className="absolute pointer-events-none z-20 bg-surface border border-border-soft rounded-lg px-2 py-1 text-xs font-bold text-text-main shadow-lg whitespace-nowrap"
|
||||||
|
style={{
|
||||||
|
left: tooltip.x,
|
||||||
|
top: above ? tooltip.y - 34 : tooltip.y + 8,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tooltip.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<TooltipState | null>(null);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(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 (
|
||||||
|
<div className="relative">
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${chartW} ${barAreaH + 18}`}
|
||||||
|
className="w-full"
|
||||||
|
onMouseLeave={() => setTooltip(null)}
|
||||||
|
>
|
||||||
|
{data.map((d, i) => {
|
||||||
|
const val = d[valueKey] as number;
|
||||||
|
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 = showLabel(i) ? getPeriodLabel(d, groupedBy) : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={i}>
|
||||||
|
<rect
|
||||||
|
x={x} y={y} width={barW} height={barH} rx={2}
|
||||||
|
fill={color}
|
||||||
|
opacity={val === 0 ? 0.12 : 0.85}
|
||||||
|
className="cursor-pointer transition-opacity hover:opacity-100"
|
||||||
|
onMouseMove={(e) => handleMouseMove(e, `${getPeriodLabel(d, groupedBy)}: ${formatValue(val)}`)}
|
||||||
|
/>
|
||||||
|
{val === 0 && (
|
||||||
|
<rect
|
||||||
|
x={x} y={0} width={barW} height={barAreaH}
|
||||||
|
fill="transparent"
|
||||||
|
onMouseMove={(e) => handleMouseMove(e, `${getPeriodLabel(d, groupedBy)}: ${formatValue(val)}`)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{label && (
|
||||||
|
<text x={x + barW / 2} y={barAreaH + 12} textAnchor="middle" fontSize="7" fontWeight="700" fill="currentColor" opacity="0.45">
|
||||||
|
{label}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
{tooltip && <ChartTooltip tooltip={tooltip} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectMiniPieChart({ data }: { data: ProjectSlice[] }) {
|
||||||
|
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const total = data.reduce((s, d) => s + d.totalMinutes, 0);
|
||||||
|
if (total === 0 || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full opacity-40 text-xs font-bold">
|
||||||
|
Sem dados
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
className="w-full h-full"
|
||||||
|
onMouseLeave={() => setTooltip(null)}
|
||||||
|
>
|
||||||
|
{slices.map((s, i) => (
|
||||||
|
<path
|
||||||
|
key={i}
|
||||||
|
d={s.path}
|
||||||
|
fill={s.color}
|
||||||
|
stroke="var(--color-surface,#fff)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
className="cursor-pointer transition-opacity hover:opacity-90"
|
||||||
|
onMouseMove={(e) => handleMouseMove(e, s.label)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
{tooltip && <ChartTooltip tooltip={tooltip} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,46 +6,18 @@ import {
|
||||||
orgDashboardService,
|
orgDashboardService,
|
||||||
type MemberDetailData,
|
type MemberDetailData,
|
||||||
type PeriodStats,
|
type PeriodStats,
|
||||||
type MemberProjectStats,
|
|
||||||
type MemberCategoryStats,
|
type MemberCategoryStats,
|
||||||
} from "../services/orgDashboardService";
|
} from "../services/orgDashboardService";
|
||||||
import { CategoryIcon } from "./CategoryIcon";
|
import { CategoryIcon } from "./CategoryIcon";
|
||||||
|
import {
|
||||||
const MONTH_NAMES = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
|
CHART_COLORS,
|
||||||
const CHART_COLORS = ["#7C3AED", "#06B6D4", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#3B82F6", "#EC4899"];
|
BarChart,
|
||||||
|
ProjectMiniPieChart,
|
||||||
function formatMinutes(minutes: number) {
|
ChartTooltip,
|
||||||
const h = Math.floor(minutes / 60);
|
chartFormatMinutes,
|
||||||
const m = minutes % 60;
|
getPeriodLabel,
|
||||||
if (h === 0) return `${m}m`;
|
} from "./DashboardCharts";
|
||||||
if (m === 0) return `${h}h`;
|
import type { TooltipState } from "./DashboardCharts";
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className="absolute pointer-events-none z-20 bg-surface border border-border-soft rounded-lg px-2 py-1 text-xs font-bold text-text-main shadow-lg whitespace-nowrap"
|
|
||||||
style={{
|
|
||||||
left: tooltip.x,
|
|
||||||
top: above ? tooltip.y - 34 : tooltip.y + 8,
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tooltip.content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Avatar({ name, photo }: { name: string; photo?: string | null }) {
|
function Avatar({ name, photo }: { name: string; photo?: string | null }) {
|
||||||
if (photo) return <img src={photo} alt={name} className="w-14 h-14 rounded-2xl object-cover shrink-0" />;
|
if (photo) return <img src={photo} alt={name} className="w-14 h-14 rounded-2xl object-cover shrink-0" />;
|
||||||
|
|
@ -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<TooltipState | null>(null);
|
|
||||||
const svgRef = useRef<SVGSVGElement>(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 (
|
|
||||||
<div className="relative">
|
|
||||||
<svg
|
|
||||||
ref={svgRef}
|
|
||||||
viewBox={`0 0 ${chartW} ${barAreaH + 18}`}
|
|
||||||
className="w-full"
|
|
||||||
onMouseLeave={() => setTooltip(null)}
|
|
||||||
>
|
|
||||||
{data.map((d, i) => {
|
|
||||||
const val = d[valueKey] as number;
|
|
||||||
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 = showLabel(i) ? getPeriodLabel(d, groupedBy) : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g key={i}>
|
|
||||||
<rect
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
width={barW}
|
|
||||||
height={barH}
|
|
||||||
rx={2}
|
|
||||||
fill={color}
|
|
||||||
opacity={val === 0 ? 0.12 : 0.85}
|
|
||||||
className="cursor-pointer transition-opacity hover:opacity-100"
|
|
||||||
onMouseMove={(e) =>
|
|
||||||
handleMouseMove(e, `${getPeriodLabel(d, groupedBy)}: ${formatValue(val)}`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/* invisible hit area for zero-height bars */}
|
|
||||||
{val === 0 && (
|
|
||||||
<rect
|
|
||||||
x={x}
|
|
||||||
y={0}
|
|
||||||
width={barW}
|
|
||||||
height={barAreaH}
|
|
||||||
fill="transparent"
|
|
||||||
onMouseMove={(e) =>
|
|
||||||
handleMouseMove(e, `${getPeriodLabel(d, groupedBy)}: ${formatValue(val)}`)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{label && (
|
|
||||||
<text
|
|
||||||
x={x + barW / 2}
|
|
||||||
y={barAreaH + 12}
|
|
||||||
textAnchor="middle"
|
|
||||||
fontSize="7"
|
|
||||||
fontWeight="700"
|
|
||||||
fill="currentColor"
|
|
||||||
opacity="0.45"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
)}
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
{tooltip && <ChartTooltip tooltip={tooltip} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
|
function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
|
||||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||||
|
|
@ -184,7 +57,7 @@ function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
|
||||||
const largeArc = angle > Math.PI ? 1 : 0;
|
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 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 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;
|
startAngle = endAngle;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
@ -220,68 +93,6 @@ function CategoryMiniPieChart({ data }: { data: MemberCategoryStats[] }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MiniPieChart({ data }: { data: MemberProjectStats[] }) {
|
|
||||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
|
||||||
|
|
||||||
const total = data.reduce((s, d) => s + d.totalMinutes, 0);
|
|
||||||
if (total === 0 || data.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-full opacity-40 text-xs font-bold">
|
|
||||||
Sem dados
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="relative w-full h-full">
|
|
||||||
<svg
|
|
||||||
ref={svgRef}
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
className="w-full h-full"
|
|
||||||
onMouseLeave={() => setTooltip(null)}
|
|
||||||
>
|
|
||||||
{slices.map((s, i) => (
|
|
||||||
<path
|
|
||||||
key={i}
|
|
||||||
d={s.path}
|
|
||||||
fill={s.color}
|
|
||||||
stroke="var(--color-surface,#fff)"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
className="cursor-pointer transition-opacity hover:opacity-90"
|
|
||||||
onMouseMove={(e) => handleMouseMove(e, s.label)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
{tooltip && <ChartTooltip tooltip={tooltip} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemberDetailModalProps {
|
interface MemberDetailModalProps {
|
||||||
memberId: number | null;
|
memberId: number | null;
|
||||||
|
|
@ -389,12 +200,12 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
||||||
<Clock size={14} className="text-brand mb-1" />
|
<Clock size={14} className="text-brand mb-1" />
|
||||||
<div className="text-lg font-black text-text-main leading-none">{formatMinutes(totalMinutes)}</div>
|
<div className="text-lg font-black text-text-main leading-none">{chartFormatMinutes(totalMinutes)}</div>
|
||||||
<div className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-1">Trabalhado</div>
|
<div className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-1">Trabalhado</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
||||||
<TrendingUp size={14} className="text-cyan-500 mb-1" />
|
<TrendingUp size={14} className="text-cyan-500 mb-1" />
|
||||||
<div className="text-lg font-black text-text-main leading-none">{formatMinutes(Math.round(avgPerDay))}</div>
|
<div className="text-lg font-black text-text-main leading-none">{chartFormatMinutes(Math.round(avgPerDay))}</div>
|
||||||
<div className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-1">Média/dia</div>
|
<div className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-1">Média/dia</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
<div className="bg-app-bg border border-border-soft rounded-xl p-3 flex flex-col items-center text-center">
|
||||||
|
|
@ -434,7 +245,7 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
||||||
valueKey="totalMinutesWorked"
|
valueKey="totalMinutesWorked"
|
||||||
color="#06B6D4"
|
color="#06B6D4"
|
||||||
groupedBy={data.groupedBy}
|
groupedBy={data.groupedBy}
|
||||||
formatValue={formatMinutes}
|
formatValue={chartFormatMinutes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -466,7 +277,7 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-bold text-text-main truncate flex-1">{c.categoryName}</span>
|
<span className="text-xs font-bold text-text-main truncate flex-1">{c.categoryName}</span>
|
||||||
<span className="text-xs font-black text-text-muted shrink-0">
|
<span className="text-xs font-black text-text-muted shrink-0">
|
||||||
{formatMinutes(c.totalMinutes)} ({pct}%)
|
{chartFormatMinutes(c.totalMinutes)} ({pct}%)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -484,7 +295,7 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<div className="w-20 h-20 shrink-0">
|
<div className="w-20 h-20 shrink-0">
|
||||||
<MiniPieChart data={data.projectStats} />
|
<ProjectMiniPieChart data={data.projectStats} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-1.5">
|
<div className="flex-1 space-y-1.5">
|
||||||
{data.projectStats
|
{data.projectStats
|
||||||
|
|
@ -498,7 +309,7 @@ export const MemberDetailModal: React.FC<MemberDetailModalProps> = ({
|
||||||
<div className="w-2 h-2 rounded-full shrink-0" style={{ background: CHART_COLORS[i % CHART_COLORS.length] }} />
|
<div className="w-2 h-2 rounded-full shrink-0" style={{ background: CHART_COLORS[i % CHART_COLORS.length] }} />
|
||||||
<span className="text-xs font-bold text-text-main truncate flex-1">{p.projectName}</span>
|
<span className="text-xs font-bold text-text-main truncate flex-1">{p.projectName}</span>
|
||||||
<span className="text-xs font-black text-text-muted shrink-0">
|
<span className="text-xs font-black text-text-muted shrink-0">
|
||||||
{formatMinutes(p.totalMinutes)} ({pct}%)
|
{chartFormatMinutes(p.totalMinutes)} ({pct}%)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue