Skip to content

Commit 741b115

Browse files
committed
feat(frameworks): add documents score calculation and update compliance overview
1 parent a57dfbd commit 741b115

File tree

12 files changed

+445
-184
lines changed

12 files changed

+445
-184
lines changed
Lines changed: 136 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,83 @@
11
'use client';
22

3+
import { Catalog, Group, ListChecked, Policy } from '@carbon/icons-react';
34
import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
4-
import { Progress } from '@comp/ui/progress';
5-
import { FrameworkInstance } from '@db';
6-
import { ComplianceProgressChart } from './ComplianceProgressChart';
7-
import { PeopleChart } from './PeopleChart';
8-
import { PoliciesChart } from './PoliciesChart';
9-
import { TasksChart } from './TasksChart';
5+
import { Button } from '@trycompai/design-system';
6+
import { ArrowRight } from '@trycompai/design-system/icons';
7+
import { useRouter } from 'next/navigation';
108

119
export function ComplianceOverview({
12-
frameworks,
10+
organizationId,
11+
overallComplianceScore,
1312
totalPolicies,
1413
publishedPolicies,
1514
totalTasks,
1615
doneTasks,
16+
totalDocuments,
17+
completedDocuments,
1718
totalMembers,
1819
completedMembers,
1920
}: {
20-
frameworks: FrameworkInstance[];
21+
organizationId: string;
22+
overallComplianceScore: number;
2123
totalPolicies: number;
2224
publishedPolicies: number;
2325
totalTasks: number;
2426
doneTasks: number;
27+
totalDocuments: number;
28+
completedDocuments: number;
2529
totalMembers: number;
2630
completedMembers: number;
2731
}) {
28-
const compliancePercentage = complianceProgress(
29-
publishedPolicies,
30-
doneTasks,
31-
totalPolicies,
32-
totalTasks,
33-
totalMembers,
34-
completedMembers,
35-
);
32+
const router = useRouter();
33+
34+
const metrics = [
35+
{
36+
id: 'policies',
37+
label: 'Policies',
38+
subtitle: `${publishedPolicies}/${totalPolicies} policies published`,
39+
percentage: getPercentage(publishedPolicies, totalPolicies),
40+
icon: Policy,
41+
total: totalPolicies,
42+
href: `/${organizationId}/policies`,
43+
},
44+
{
45+
id: 'tasks',
46+
label: 'Evidence',
47+
subtitle: `${doneTasks}/${totalTasks} evidence tasks complete`,
48+
percentage: getPercentage(doneTasks, totalTasks),
49+
icon: ListChecked,
50+
total: totalTasks,
51+
href: `/${organizationId}/tasks`,
52+
},
53+
{
54+
id: 'documents',
55+
label: 'Documents',
56+
subtitle: `${completedDocuments}/${totalDocuments} documents up to date`,
57+
percentage: getPercentage(completedDocuments, totalDocuments),
58+
icon: Catalog,
59+
total: totalDocuments,
60+
href: `/${organizationId}/documents`,
61+
},
62+
{
63+
id: 'people',
64+
label: 'People',
65+
subtitle: `${completedMembers}/${totalMembers} people complete`,
66+
percentage: getPercentage(completedMembers, totalMembers),
67+
icon: Group,
68+
total: totalMembers,
69+
href: `/${organizationId}/people/all`,
70+
},
71+
] as const;
3672

37-
const policiesPercentage = Math.round((publishedPolicies / Math.max(totalPolicies, 1)) * 100);
38-
const tasksPercentage = Math.round((doneTasks / Math.max(totalTasks, 1)) * 100);
39-
const peoplePercentage = Math.round((completedMembers / Math.max(totalMembers, 1)) * 100);
73+
const compliancePercentage = overallComplianceScore;
4074

4175
return (
42-
<Card className="flex flex-col overflow-hidden border h-full">
43-
<CardHeader className="pb-2">
76+
<Card className="flex h-full flex-col overflow-hidden border">
77+
<CardHeader className="pb-3">
4478
<div className="flex items-center justify-between">
45-
<CardTitle className="flex items-center gap-2">{'Overall Compliance Progress'}</CardTitle>
79+
<CardTitle className="flex items-center gap-2">Overall Compliance Progress</CardTitle>
4680
</div>
47-
4881
<div className="bg-secondary/50 relative mt-2 h-1 w-full overflow-hidden rounded-full">
4982
<div
5083
className="bg-primary h-full transition-all"
@@ -54,107 +87,93 @@ export function ComplianceOverview({
5487
/>
5588
</div>
5689
</CardHeader>
57-
<CardContent className="flex flex-col flex-1 justify-center">
58-
{/* Progress bars for smaller screens */}
59-
<div className="space-y-4 lg:hidden mt-4">
60-
{/* Overall Compliance Progress Bar */}
61-
<div className="space-y-3">
62-
<div className="flex items-center justify-between">
63-
<div className="flex items-center gap-2">
64-
<div className="h-2 w-2 rounded-full bg-primary"></div>
65-
<span className="text-sm">Overall Compliance</span>
66-
</div>
67-
<span className="font-medium text-sm tabular-nums">{compliancePercentage}%</span>
68-
</div>
69-
<Progress value={compliancePercentage} className="h-1" />
70-
</div>
71-
72-
{/* Policies Progress Bar */}
73-
<div className="space-y-3">
74-
<div className="flex items-center justify-between">
75-
<div className="flex items-center gap-2">
76-
<div className="h-2 w-2 rounded-full bg-blue-500"></div>
77-
<span className="text-sm">Policies Published</span>
78-
</div>
79-
<span className="font-medium text-sm tabular-nums">{policiesPercentage}%</span>
80-
</div>
81-
<Progress value={policiesPercentage} className="h-1" />
82-
</div>
83-
84-
{/* Tasks Progress Bar */}
85-
<div className="space-y-3">
86-
<div className="flex items-center justify-between">
87-
<div className="flex items-center gap-2">
88-
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
89-
<span className="text-sm">Tasks Completed</span>
90-
</div>
91-
<span className="font-medium text-sm tabular-nums">{tasksPercentage}%</span>
92-
</div>
93-
<Progress value={tasksPercentage} className="h-1" />
94-
</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>
90+
<CardContent className="flex flex-1 flex-col p-0">
91+
<div className="divide-y divide-border">
92+
{metrics.map((metric) => {
93+
const Icon = metric.icon;
94+
return (
95+
<div key={metric.id} className="flex items-center justify-between px-4 py-3">
96+
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
97+
<div className="hidden h-9 w-9 place-items-center rounded-md border border-border/70 bg-muted/30 sm:grid">
98+
<Icon className="h-4 w-4 text-foreground" />
99+
</div>
100+
<div className="min-w-0">
101+
<p className="truncate text-sm font-medium text-foreground">{metric.label}</p>
102+
<p className="truncate text-xs text-muted-foreground">{metric.subtitle}</p>
103+
{metric.percentage < 100 && (
104+
<div className="mt-1 md:hidden">
105+
<Button variant="link" onClick={() => router.push(metric.href)}>
106+
Continue
107+
</Button>
108+
</div>
109+
)}
110+
</div>
111+
</div>
112+
<div className="flex items-center gap-3">
113+
{metric.percentage < 100 && (
114+
<div className="hidden md:block">
115+
<Button
116+
size="sm"
117+
variant="outline"
118+
iconRight={<ArrowRight size={14} />}
119+
onClick={() => router.push(metric.href)}
120+
>
121+
Continue
122+
</Button>
123+
</div>
124+
)}
125+
<MiniProgressRing percentage={metric.percentage} />
126+
</div>
102127
</div>
103-
<span className="font-medium text-sm tabular-nums">{peoplePercentage}%</span>
104-
</div>
105-
<Progress value={peoplePercentage} className="h-1" />
106-
</div>
107-
</div>
108-
109-
{/* Charts for larger screens */}
110-
<div className="hidden lg:flex lg:flex-col lg:items-center lg:justify-center lg:gap-4">
111-
<ComplianceProgressChart
112-
data={{ score: compliancePercentage, remaining: 100 - compliancePercentage }}
113-
/>
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>
128-
</div>
128+
);
129+
})}
129130
</div>
130131
</CardContent>
131132
</Card>
132133
);
133134
}
134135

135-
function complianceProgress(
136-
publishedPolicies: number,
137-
doneTasks: number,
138-
totalPolicies: number,
139-
totalTasks: number,
140-
totalMembers: number,
141-
completedMembers: number,
142-
) {
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;
152-
153-
if (totalCategories === 0) return 0;
136+
function getPercentage(done: number, total: number): number {
137+
if (total <= 0) return 0;
138+
return Math.round((done / total) * 100);
139+
}
154140

155-
const averagePercentage =
156-
(policiesPercentage + tasksPercentage + peoplePercentage) / totalCategories;
157-
const complianceScore = Math.round(averagePercentage * 100);
141+
function MiniProgressRing({ percentage }: { percentage: number }) {
142+
const clamped = Math.max(0, Math.min(100, percentage));
143+
const radius = 18;
144+
const stroke = 4;
145+
const normalizedRadius = radius - stroke / 2;
146+
const circumference = normalizedRadius * 2 * Math.PI;
147+
const strokeDashoffset = circumference - (clamped / 100) * circumference;
158148

159-
return complianceScore;
149+
return (
150+
<div className="relative h-14 w-14 shrink-0">
151+
<svg className="h-14 w-14 -rotate-90" viewBox="0 0 36 36">
152+
<circle
153+
cx="18"
154+
cy="18"
155+
r={normalizedRadius}
156+
stroke="currentColor"
157+
strokeWidth={stroke}
158+
fill="transparent"
159+
className="text-muted/70"
160+
/>
161+
<circle
162+
cx="18"
163+
cy="18"
164+
r={normalizedRadius}
165+
stroke="currentColor"
166+
strokeWidth={stroke}
167+
fill="transparent"
168+
strokeDasharray={circumference}
169+
strokeDashoffset={strokeDashoffset}
170+
strokeLinecap="round"
171+
className="text-primary transition-all duration-500 ease-out"
172+
/>
173+
</svg>
174+
<div className="absolute inset-0 grid place-items-center text-[11px] font-semibold tabular-nums text-foreground">
175+
{clamped}%
176+
</div>
177+
</div>
178+
);
160179
}

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface FrameworksOverviewProps {
1717
allFrameworks: FrameworkEditorFramework[];
1818
frameworksWithCompliance?: FrameworkInstanceWithComplianceScore[];
1919
organizationId?: string;
20+
overallComplianceScore: number;
2021
}
2122

2223
export function mapFrameworkToBadge(framework: FrameworkInstanceWithControls) {
@@ -58,6 +59,7 @@ export function mapFrameworkToBadge(framework: FrameworkInstanceWithControls) {
5859
export function FrameworksOverview({
5960
frameworksWithControls,
6061
frameworksWithCompliance,
62+
overallComplianceScore,
6163
allFrameworks,
6264
organizationId,
6365
}: FrameworksOverviewProps) {
@@ -68,13 +70,6 @@ export function FrameworksOverview({
6870
frameworksWithCompliance?.map((f) => [f.frameworkInstance.id, f.complianceScore]) ?? [],
6971
);
7072

71-
// Calculate overall compliance score from all frameworks
72-
const overallComplianceScore =
73-
frameworksWithCompliance && frameworksWithCompliance.length > 0
74-
? frameworksWithCompliance.reduce((sum, f) => sum + f.complianceScore, 0) /
75-
frameworksWithCompliance.length
76-
: 0;
77-
7873
// Get available frameworks that can be added (not already in the organization)
7974
const availableFrameworksToAdd = allFrameworks.filter(
8075
(framework) => !frameworksWithControls.some((fc) => fc.framework.id === framework.id),
@@ -108,7 +103,7 @@ export function FrameworksOverview({
108103
<div key={framework.id}>
109104
<div className="flex items-start justify-between py-4 px-1">
110105
<div className="flex items-start gap-3 flex-1 min-w-0">
111-
<div className="flex-shrink-0 mt-1">
106+
<div className="shrink-0 mt-1">
112107
{badgeSrc ? (
113108
<Image
114109
src={badgeSrc}

0 commit comments

Comments
 (0)