diff --git a/src/app/member/[id]/_components/member-goals-chart.tsx b/src/app/member/[id]/_components/member-goals-chart.tsx index 04f1e7f..563e2e1 100644 --- a/src/app/member/[id]/_components/member-goals-chart.tsx +++ b/src/app/member/[id]/_components/member-goals-chart.tsx @@ -1,11 +1,11 @@ "use client"; -import { GoalsRadarChart } from "@/components/charts"; +import { GoalsBarChart } from "@/components/charts"; interface MemberGoalsChartProps { metricIds: string[]; } export function MemberGoalsChart({ metricIds }: MemberGoalsChartProps) { - return ; + return ; } diff --git a/src/app/member/member-card.tsx b/src/app/member/member-card.tsx index cf5724f..250a47c 100644 --- a/src/app/member/member-card.tsx +++ b/src/app/member/member-card.tsx @@ -1,23 +1,31 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import Link from "next/link"; -import { ArrowRight, Users } from "lucide-react"; +import { ArrowRight, ChevronDown, ChevronUp, LayoutGrid } from "lucide-react"; -import { GoalsRadarChart, MetricPieChart } from "@/components/charts"; +import { GoalsBarChart, MetricPieChart } from "@/components/charts"; +import { KpiCard } from "@/components/metric/kpi-card"; +import { RoleCard, type RoleCardData } from "@/components/role/role-card"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import type { ChartConfig } from "@/components/ui/chart"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Skeleton } from "@/components/ui/skeleton"; import type { RouterOutputs } from "@/trpc/react"; import { api } from "@/trpc/react"; type Member = RouterOutputs["organization"]["getMembers"][number]; type DashboardCharts = RouterOutputs["dashboard"]["getDashboardCharts"]; +type Role = RouterOutputs["role"]["getByUser"][number]; interface MemberCardProps { member: Member; @@ -39,7 +47,27 @@ function getInitials(member: Member): string { return member.email.slice(0, 2).toUpperCase(); } +function roleToCardData(role: Role): RoleCardData { + return { + id: role.id, + title: role.title, + purpose: role.purpose ?? "", + color: role.color ?? "#3b82f6", + effortPoints: role.effortPoints, + assignedUserId: role.assignedUserId, + assignedUserName: null, + metric: role.metric + ? { + name: role.metric.name, + dashboardCharts: role.metric.dashboardCharts, + } + : null, + }; +} + export function MemberCard({ member, dashboardCharts }: MemberCardProps) { + const [isExpanded, setIsExpanded] = useState(false); + const { data: roles, isLoading } = api.role.getByUser.useQuery({ userId: member.id, }); @@ -67,6 +95,17 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { return Array.from(teamMap.values()); }, [roles]); + // Filter KPIs for this member (metrics linked to their roles) + const memberKpis = useMemo(() => { + if (!roles || dashboardCharts.length === 0) return []; + const memberMetricIds = new Set( + roles.filter((r) => r.metricId != null).map((r) => r.metricId!), + ); + return dashboardCharts.filter((chart) => + memberMetricIds.has(chart.metric.id), + ); + }, [roles, dashboardCharts]); + const pieChartData = rolesWithEffort.map((role, index) => ({ name: role.title, value: role.effortPoints!, @@ -87,31 +126,35 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { ?.filter((role) => role.metricId != null) .map((role) => role.metricId!) ?? []; + const hasRolesOrKpis = (roles?.length ?? 0) > 0 || memberKpis.length > 0; + return ( - -
-
-
- + +
+ {/* Left Column - Member Info */} +
+
+ {member.profilePictureUrl && ( )} - + {getInitials(member)}
-

+

{getDisplayName(member)}

-

+

{member.email}

+ {isLoading ? (
@@ -120,19 +163,25 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { ) : ( <>
- + {roles?.length ?? 0} {roles?.length === 1 ? "role" : "roles"} - {totalEffortPoints} pts + + {totalEffortPoints} pts +
+ {uniqueTeams.length > 0 && ( -
- +
+
+ + Teams: +
{uniqueTeams.map((team) => ( {team.name} @@ -142,6 +191,7 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { )} )} +
+ {/* Right Column - Charts */}
{isLoading ? ( <> @@ -163,8 +214,8 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { ) : ( <> -
- +
+ Effort Distribution {rolesWithEffort.length > 0 ? ( @@ -179,7 +230,7 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { value: totalEffortPoints, label: "Total", }} - className="h-[280px] w-full" + className="h-[260px] w-full" /> ) : (
@@ -188,15 +239,81 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) { )}
- )}
+ + {/* Expandable Section for Roles & KPIs */} + {hasRolesOrKpis && ( + + + + + + + {/* Roles Section */} + {roles && roles.length > 0 && ( +
+

+ Roles ({roles.length}) +

+
+ {roles.map((role) => ( + + ))} +
+
+ )} + + {/* KPIs Section */} + {memberKpis.length > 0 && ( +
+

+ KPIs ({memberKpis.length}) +

+
+ {memberKpis.map((chart) => ( + + ))} +
+
+ )} +
+
+ )} ); } diff --git a/src/components/charts/goals-radar-chart.tsx b/src/components/charts/goals-bar-chart.tsx similarity index 78% rename from src/components/charts/goals-radar-chart.tsx rename to src/components/charts/goals-bar-chart.tsx index c50dd34..3020420 100644 --- a/src/components/charts/goals-radar-chart.tsx +++ b/src/components/charts/goals-bar-chart.tsx @@ -32,11 +32,13 @@ import { formatValue } from "@/lib/helpers/format-value"; import { cn } from "@/lib/utils"; import { api } from "@/trpc/react"; -interface GoalsRadarChartProps { +interface GoalsBarChartProps { /** Array of metric IDs to display goal progress for */ metricIds: string[]; showHeader?: boolean; className?: string; + /** Use simplified legend with only colored dots and names (no status badges, percentages, or tooltips) */ + simpleLegend?: boolean; } const STATUS_CONFIG: Record< @@ -284,11 +286,12 @@ function GoalTooltipContent({ active, payload }: GoalTooltipProps) { ); } -export function GoalsRadarChart({ +export function GoalsBarChart({ metricIds, showHeader = true, className, -}: GoalsRadarChartProps) { + simpleLegend = false, +}: GoalsBarChartProps) { // Fetch dashboard charts from cache (parent already fetched this) const { data: allCharts } = api.dashboard.getDashboardCharts.useQuery(); @@ -429,63 +432,86 @@ export function GoalsRadarChart({ -
- {chartData.map((item) => { - const statusConfig = STATUS_CONFIG[item.status]; - return ( - - - -
-
-
- - {item.goal} - -
- - {Math.round(item.progress)}% + {simpleLegend ? ( +
+ {chartData.map((item) => ( +
+
+ + {item.goal} + +
+ ))} +
+ ) : ( +
+ {chartData.map((item) => { + const statusConfig = STATUS_CONFIG[item.status]; + return ( + + + +
+
+
+ + {item.goal} - - {statusConfig.icon} - {statusConfig.label} - +
+ + {Math.round(item.progress)}% + + + {statusConfig.icon} + {statusConfig.label} + +
-
-
- -
-

{item.goal}

- {item.selectedDimension && ( -

- Dimension: {item.selectedDimension} -

- )} -
- Progress: - - {Math.round(item.progress)}% - + + +
+

{item.goal}

+ {item.selectedDimension && ( +

+ Dimension: {item.selectedDimension} +

+ )} +
+ + Progress: + + + {Math.round(item.progress)}% + +
-
- - - - ); - })} -
+
+
+
+ ); + })} +
+ )}
); diff --git a/src/components/charts/index.ts b/src/components/charts/index.ts index 778cd77..9d688de 100644 --- a/src/components/charts/index.ts +++ b/src/components/charts/index.ts @@ -1,6 +1,6 @@ export { MetricAreaChart } from "./area-chart"; export { MetricBarChart } from "./bar-chart"; -export { GoalsRadarChart } from "./goals-radar-chart"; +export { GoalsBarChart } from "./goals-bar-chart"; export { MetricPieChart } from "./pie-chart"; export { MetricRadarChart } from "./radar-chart"; export { MetricRadialChart } from "./radial-chart"; diff --git a/src/components/charts/pie-chart.tsx b/src/components/charts/pie-chart.tsx index 36051fa..5166f5e 100644 --- a/src/components/charts/pie-chart.tsx +++ b/src/components/charts/pie-chart.tsx @@ -89,7 +89,10 @@ export function MetricPieChart({ )} {showLegend && ( - } /> + } + wrapperStyle={{ paddingTop: 16 }} + /> )}