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
4 changes: 2 additions & 2 deletions src/app/member/[id]/_components/member-goals-chart.tsx
Original file line number Diff line number Diff line change
@@ -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 <GoalsRadarChart metricIds={metricIds} showHeader={true} />;
return <GoalsBarChart metricIds={metricIds} showHeader={true} />;
}
159 changes: 138 additions & 21 deletions src/app/member/member-card.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
});
Expand Down Expand Up @@ -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!,
Expand All @@ -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 (
<Card className="p-6">
<div className="flex flex-col gap-6 md:flex-row">
<div className="flex w-full flex-col gap-4 md:w-[220px] md:shrink-0">
<div className="flex items-center gap-4">
<Avatar className="h-14 w-14 shrink-0">
<Card className="p-5">
<div className="flex flex-col gap-5 md:flex-row">
{/* Left Column - Member Info */}
<div className="flex w-full flex-col gap-3 md:w-[220px] md:shrink-0">
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12 shrink-0">
{member.profilePictureUrl && (
<AvatarImage
src={member.profilePictureUrl}
alt={getDisplayName(member)}
/>
)}
<AvatarFallback className="bg-primary/10 text-primary text-lg">
<AvatarFallback className="bg-primary/10 text-primary text-base font-medium">
{getInitials(member)}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<h3 className="truncate text-lg font-semibold">
<h3 className="truncate text-base leading-tight font-semibold">
{getDisplayName(member)}
</h3>
<p className="text-muted-foreground truncate text-sm">
<p className="text-muted-foreground/80 truncate text-sm">
{member.email}
</p>
</div>
</div>

{isLoading ? (
<div className="flex gap-2">
<Skeleton className="h-5 w-16" />
Expand All @@ -120,19 +163,25 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
) : (
<>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">
<Badge variant="secondary" className="text-xs">
{roles?.length ?? 0} {roles?.length === 1 ? "role" : "roles"}
</Badge>
<Badge variant="outline">{totalEffortPoints} pts</Badge>
<Badge variant="outline" className="text-xs">
{totalEffortPoints} pts
</Badge>
</div>

{uniqueTeams.length > 0 && (
<div className="flex flex-wrap items-center gap-1.5">
<Users className="text-muted-foreground h-3.5 w-3.5" />
<div className="flex flex-wrap items-center gap-2">
<div className="text-muted-foreground flex items-center gap-1.5">
<LayoutGrid className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Teams:</span>
</div>
{uniqueTeams.map((team) => (
<Link key={team.id} href={`/teams/${team.id}`}>
<Badge
variant="outline"
className="hover:bg-accent cursor-pointer text-xs transition-colors"
className="hover:bg-accent hover:border-accent-foreground/20 cursor-pointer text-xs transition-colors"
>
{team.name}
</Badge>
Expand All @@ -142,6 +191,7 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
)}
</>
)}

<Button
asChild
variant="outline"
Expand All @@ -155,6 +205,7 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
</Button>
</div>

{/* Right Column - Charts */}
<div className="grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
{isLoading ? (
<>
Expand All @@ -163,8 +214,8 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
</>
) : (
<>
<div className="border-border/40 flex h-[380px] flex-col rounded-md border p-4">
<span className="text-muted-foreground mb-2 text-xs font-medium tracking-wider uppercase">
<div className="border-border/40 flex h-[340px] flex-col rounded-lg border p-4">
<span className="text-muted-foreground mb-2 text-xs font-semibold tracking-wider uppercase">
Effort Distribution
</span>
{rolesWithEffort.length > 0 ? (
Expand All @@ -179,7 +230,7 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
value: totalEffortPoints,
label: "Total",
}}
className="h-[280px] w-full"
className="h-[260px] w-full"
/>
) : (
<div className="text-muted-foreground flex flex-1 items-center justify-center text-sm">
Expand All @@ -188,15 +239,81 @@ export function MemberCard({ member, dashboardCharts }: MemberCardProps) {
)}
</div>

<GoalsRadarChart
<GoalsBarChart
metricIds={metricIdsWithGoals}
showHeader={false}
className="h-[380px] rounded-md"
simpleLegend={true}
className="h-[340px] rounded-lg"
/>
</>
)}
</div>
</div>

{/* Expandable Section for Roles & KPIs */}
{hasRolesOrKpis && (
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground mt-4 w-full justify-center gap-2"
>
{isExpanded ? (
<>
<ChevronUp className="h-4 w-4" />
<span className="text-xs font-medium">Hide Roles & KPIs</span>
</>
) : (
<>
<ChevronDown className="h-4 w-4" />
<span className="text-xs font-medium">Show Roles & KPIs</span>
</>
)}
</Button>
</CollapsibleTrigger>

<CollapsibleContent className="mt-4 space-y-6">
{/* Roles Section */}
{roles && roles.length > 0 && (
<div className="space-y-3">
<h4 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Roles ({roles.length})
</h4>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{roles.map((role) => (
<RoleCard
key={role.id}
role={roleToCardData(role)}
readOnly={true}
/>
))}
</div>
</div>
)}

{/* KPIs Section */}
{memberKpis.length > 0 && (
<div className="space-y-3">
<h4 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
KPIs ({memberKpis.length})
</h4>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{memberKpis.map((chart) => (
<KpiCard
key={chart.id}
dashboardChart={chart}
teamId={chart.metric.teamId ?? ""}
showSettings={false}
enableDragDrop={false}
/>
))}
</div>
</div>
)}
</CollapsibleContent>
</Collapsible>
)}
</Card>
);
}
Loading