Skip to content

Commit fbbe666

Browse files
feat(frameworks): add people score calculation and visualization (#1842)
Co-authored-by: Mariano Fuentes <[email protected]>
1 parent 02d1919 commit fbbe666

File tree

9 files changed

+341
-45
lines changed

9 files changed

+341
-45
lines changed

apps/app/src/app/(app)/[orgId]/frameworks/components/ComplianceOverview.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
44
import { Progress } from '@comp/ui/progress';
55
import { FrameworkInstance } from '@db';
66
import { ComplianceProgressChart } from './ComplianceProgressChart';
7+
import { PeopleChart } from './PeopleChart';
78
import { PoliciesChart } from './PoliciesChart';
89
import { TasksChart } from './TasksChart';
910

@@ -13,22 +14,29 @@ export function ComplianceOverview({
1314
publishedPolicies,
1415
totalTasks,
1516
doneTasks,
17+
totalMembers,
18+
completedMembers,
1619
}: {
1720
frameworks: FrameworkInstance[];
1821
totalPolicies: number;
1922
publishedPolicies: number;
2023
totalTasks: number;
2124
doneTasks: number;
25+
totalMembers: number;
26+
completedMembers: number;
2227
}) {
2328
const compliancePercentage = complianceProgress(
2429
publishedPolicies,
2530
doneTasks,
2631
totalPolicies,
2732
totalTasks,
33+
totalMembers,
34+
completedMembers,
2835
);
2936

3037
const policiesPercentage = Math.round((publishedPolicies / Math.max(totalPolicies, 1)) * 100);
3138
const tasksPercentage = Math.round((doneTasks / Math.max(totalTasks, 1)) * 100);
39+
const peoplePercentage = Math.round((completedMembers / Math.max(totalMembers, 1)) * 100);
3240

3341
return (
3442
<Card className="flex flex-col overflow-hidden border h-full">
@@ -46,7 +54,7 @@ export function ComplianceOverview({
4654
/>
4755
</div>
4856
</CardHeader>
49-
<CardContent className="flex flex-col gap-3">
57+
<CardContent className="flex flex-col flex-1 justify-center">
5058
{/* Progress bars for smaller screens */}
5159
<div className="space-y-4 lg:hidden mt-4">
5260
{/* Overall Compliance Progress Bar */}
@@ -84,23 +92,39 @@ export function ComplianceOverview({
8492
</div>
8593
<Progress value={tasksPercentage} className="h-1" />
8694
</div>
95+
96+
{/* People Progress Bar */}
97+
<div className="space-y-3">
98+
<div className="flex items-center justify-between">
99+
<div className="flex items-center gap-2">
100+
<div className="h-2 w-2 rounded-full bg-green-500"></div>
101+
<span className="text-sm">People Score</span>
102+
</div>
103+
<span className="font-medium text-sm tabular-nums">{peoplePercentage}%</span>
104+
</div>
105+
<Progress value={peoplePercentage} className="h-1" />
106+
</div>
87107
</div>
88108

89109
{/* Charts for larger screens */}
90-
<div className="hidden lg:flex lg:flex-col lg:items-center lg:justify-center">
110+
<div className="hidden lg:flex lg:flex-col lg:items-center lg:justify-center lg:gap-4">
91111
<ComplianceProgressChart
92112
data={{ score: compliancePercentage, remaining: 100 - compliancePercentage }}
93113
/>
94-
</div>
95-
96-
<div className="hidden lg:flex lg:flex-col lg:items-center lg:justify-center lg:gap-3 lg:flex-row lg:gap-6">
97-
<div className="flex flex-col items-center justify-center">
98-
<PoliciesChart
99-
data={{ published: policiesPercentage, draft: 100 - policiesPercentage }}
100-
/>
101-
</div>
102-
<div className="flex flex-col items-center justify-center">
103-
<TasksChart data={{ done: tasksPercentage, remaining: 100 - tasksPercentage }} />
114+
<div className="flex flex-row items-center justify-center gap-3">
115+
<div className="flex flex-col items-center justify-center">
116+
<PoliciesChart
117+
data={{ published: policiesPercentage, draft: 100 - policiesPercentage }}
118+
/>
119+
</div>
120+
<div className="flex flex-col items-center justify-center">
121+
<TasksChart data={{ done: tasksPercentage, remaining: 100 - tasksPercentage }} />
122+
</div>
123+
<div className="flex flex-col items-center justify-center">
124+
<PeopleChart
125+
data={{ completed: peoplePercentage, remaining: 100 - peoplePercentage }}
126+
/>
127+
</div>
104128
</div>
105129
</div>
106130
</CardContent>
@@ -113,13 +137,24 @@ function complianceProgress(
113137
doneTasks: number,
114138
totalPolicies: number,
115139
totalTasks: number,
140+
totalMembers: number,
141+
completedMembers: number,
116142
) {
117-
const totalItems = totalPolicies + totalTasks;
143+
// Calculate individual percentages
144+
const policiesPercentage = totalPolicies > 0 ? publishedPolicies / totalPolicies : 0;
145+
const tasksPercentage = totalTasks > 0 ? doneTasks / totalTasks : 0;
146+
const peoplePercentage = totalMembers > 0 ? completedMembers / totalMembers : 0;
147+
148+
// Calculate average of the three percentages
149+
const totalCategories = [totalPolicies, totalTasks, totalMembers].filter(
150+
(count) => count > 0,
151+
).length;
118152

119-
if (totalItems === 0) return 0;
153+
if (totalCategories === 0) return 0;
120154

121-
const completedItems = publishedPolicies + doneTasks;
122-
const complianceScore = Math.round((completedItems / totalItems) * 100);
155+
const averagePercentage =
156+
(policiesPercentage + tasksPercentage + peoplePercentage) / totalCategories;
157+
const complianceScore = Math.round(averagePercentage * 100);
123158

124159
return complianceScore;
125160
}

apps/app/src/app/(app)/[orgId]/frameworks/components/ComplianceProgressChart.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ export function ComplianceProgressChart({ data }: ComplianceProgressChartProps)
7373
} satisfies ChartConfig;
7474

7575
return (
76-
<ChartContainer config={chartConfig} className="mx-auto h-[160px] max-w-[200px]">
76+
<ChartContainer config={chartConfig} className="mx-auto h-[120px] max-w-[150px]">
7777
<PieChart
78-
width={200}
79-
height={160}
78+
width={120}
79+
height={120}
8080
margin={{
8181
top: 0,
8282
right: 0,
@@ -89,8 +89,8 @@ export function ComplianceProgressChart({ data }: ComplianceProgressChartProps)
8989
data={chartData}
9090
dataKey="value"
9191
nameKey="name"
92-
innerRadius={45}
93-
outerRadius={62}
92+
innerRadius={35}
93+
outerRadius={50}
9494
paddingAngle={2}
9595
strokeWidth={2}
9696
cursor="pointer"
@@ -111,22 +111,22 @@ export function ComplianceProgressChart({ data }: ComplianceProgressChartProps)
111111
<tspan
112112
x={viewBox.cx}
113113
y={viewBox.cy}
114-
className="fill-foreground text-lg font-medium select-none"
114+
className="fill-foreground text-base font-medium select-none"
115115
>
116116
{data.score}%
117117
</tspan>
118118
<tspan
119119
x={viewBox.cx}
120-
y={(viewBox.cy || 0) + 22}
121-
className="fill-muted-foreground text-[10px] select-none"
120+
y={(viewBox.cy || 0) + 18}
121+
className="fill-muted-foreground text-[9px] select-none"
122122
>
123123
Overall
124124
</tspan>
125125
</text>
126126
<circle
127127
cx={viewBox.cx}
128128
cy={viewBox.cy}
129-
r={42}
129+
r={32}
130130
fill="none"
131131
stroke="hsl(var(--border))"
132132
strokeWidth={1}

apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function FrameworksOverview({
8181
);
8282

8383
return (
84-
<Card className="flex flex-col h-full">
84+
<Card className="flex flex-col overflow-hidden border h-full">
8585
<CardHeader className="pb-2">
8686
<div className="flex items-center justify-between">
8787
<CardTitle className="flex items-center gap-2">{'Frameworks'}</CardTitle>

apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ export interface DoneTasksScore {
2222
incompleteTasks: Task[];
2323
}
2424

25+
export interface PeopleScore {
26+
totalMembers: number;
27+
completedMembers: number;
28+
}
29+
2530
export interface OverviewProps {
2631
frameworksWithControls: FrameworkInstanceWithControls[];
2732
frameworksWithCompliance: FrameworkInstanceWithComplianceScore[];
2833
allFrameworks: FrameworkEditorFramework[];
2934
organizationId: string;
3035
publishedPoliciesScore: PublishedPoliciesScore;
3136
doneTasksScore: DoneTasksScore;
37+
peopleScore: PeopleScore;
3238
currentMember: { id: string; role: string } | null;
3339
}
3440

@@ -39,6 +45,7 @@ export const Overview = ({
3945
organizationId,
4046
publishedPoliciesScore,
4147
doneTasksScore,
48+
peopleScore,
4249
currentMember,
4350
}: OverviewProps) => {
4451
return (
@@ -49,6 +56,8 @@ export const Overview = ({
4956
publishedPolicies={publishedPoliciesScore.publishedPolicies}
5057
totalTasks={doneTasksScore.totalTasks}
5158
doneTasks={doneTasksScore.doneTasks}
59+
totalMembers={peopleScore.totalMembers}
60+
completedMembers={peopleScore.completedMembers}
5261
/>
5362
<FrameworksOverview
5463
frameworksWithControls={frameworksWithControls}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import { Label, Pie, PieChart } from 'recharts';
5+
6+
import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
7+
import {
8+
type ChartConfig,
9+
ChartContainer,
10+
ChartTooltip,
11+
ChartTooltipContent,
12+
} from '@comp/ui/chart';
13+
import { Info } from 'lucide-react';
14+
15+
interface PeopleChartData {
16+
completed: number;
17+
remaining: number;
18+
}
19+
20+
interface PeopleChartProps {
21+
data?: PeopleChartData | null;
22+
}
23+
24+
const CHART_COLORS = {
25+
completed: 'hsl(var(--chart-primary))',
26+
remaining: 'hsl(var(--muted))',
27+
};
28+
29+
export function PeopleChart({ data }: PeopleChartProps) {
30+
const chartData = React.useMemo(() => {
31+
if (!data) return [];
32+
const items = [
33+
{
34+
name: 'Compliant',
35+
value: data.completed,
36+
fill: CHART_COLORS.completed,
37+
},
38+
{
39+
name: 'Remaining',
40+
value: data.remaining,
41+
fill: CHART_COLORS.remaining,
42+
},
43+
];
44+
return items.filter((item) => item.value > 0);
45+
}, [data]);
46+
47+
if (!data) {
48+
return (
49+
<Card className="flex flex-col overflow-hidden border">
50+
<CardHeader className="pb-2">
51+
<div className="flex items-center justify-between">
52+
<CardTitle className="flex items-center gap-2">People</CardTitle>
53+
</div>
54+
</CardHeader>
55+
<CardContent className="flex flex-1 items-center justify-center py-10">
56+
<div className="space-y-2 text-center">
57+
<div className="text-muted-foreground flex justify-center">
58+
<Info className="h-10 w-10 opacity-30" />
59+
</div>
60+
<p className="text-muted-foreground text-center text-sm">No data available</p>
61+
</div>
62+
</CardContent>
63+
</Card>
64+
);
65+
}
66+
67+
const chartConfig = {
68+
value: {
69+
label: 'People Status',
70+
},
71+
} satisfies ChartConfig;
72+
73+
return (
74+
<ChartContainer config={chartConfig} className="mx-auto h-[120px] max-w-[150px]">
75+
<PieChart
76+
width={120}
77+
height={120}
78+
margin={{
79+
top: 0,
80+
right: 0,
81+
bottom: 0,
82+
left: 0,
83+
}}
84+
>
85+
<ChartTooltip cursor={false} content={<ChartTooltipContent isPercentage={true} />} />
86+
<Pie
87+
data={chartData}
88+
dataKey="value"
89+
nameKey="name"
90+
innerRadius={35}
91+
outerRadius={50}
92+
paddingAngle={2}
93+
strokeWidth={2}
94+
cursor="pointer"
95+
animationDuration={500}
96+
animationBegin={100}
97+
>
98+
<Label
99+
content={({ viewBox }) => {
100+
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
101+
return (
102+
<g>
103+
<text
104+
x={viewBox.cx}
105+
y={viewBox.cy}
106+
textAnchor="middle"
107+
dominantBaseline="middle"
108+
>
109+
<tspan
110+
x={viewBox.cx}
111+
y={viewBox.cy}
112+
className="fill-foreground text-base font-medium select-none"
113+
>
114+
{data.completed}%
115+
</tspan>
116+
<tspan
117+
x={viewBox.cx}
118+
y={(viewBox.cy || 0) + 18}
119+
className="fill-muted-foreground text-[9px] select-none"
120+
>
121+
People
122+
</tspan>
123+
</text>
124+
<circle
125+
cx={viewBox.cx}
126+
cy={viewBox.cy}
127+
r={32}
128+
fill="none"
129+
stroke="hsl(var(--border))"
130+
strokeWidth={1}
131+
strokeDasharray="2,2"
132+
/>
133+
</g>
134+
);
135+
}
136+
return null;
137+
}}
138+
/>
139+
</Pie>
140+
</PieChart>
141+
</ChartContainer>
142+
);
143+
}

0 commit comments

Comments
 (0)