Skip to content

Commit 0a2d766

Browse files
fix: fix progress counts for frameworks, requirements and controls
[dev] [Marfuen] mariano/cs-316-bug-progress-tracking-issues-and-missing-documents-link-in
1 parent 8e4ca07 commit 0a2d766

7 files changed

Lines changed: 748 additions & 89 deletions

File tree

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,15 @@ import {
1616
} from '@trycompai/ui/dropdown-menu';
1717
import { useState } from 'react';
1818
import { usePermissions } from '@/hooks/use-permissions';
19-
import { getControlStatus } from '@/lib/control-compliance';
19+
import {
20+
type EvidenceSubmissionInfo,
21+
getControlStatus,
22+
} from '@/lib/control-compliance';
2023
import type { FrameworkInstanceWithControls } from '@/lib/types/framework';
2124
import { FrameworkDeleteDialog } from './FrameworkDeleteDialog';
2225
import { AddCustomRequirementSheet } from './AddCustomRequirementSheet';
2326
import { LinkRequirementSheet } from './LinkRequirementSheet';
2427

25-
interface EvidenceSubmissionInfo {
26-
id: string;
27-
formType: string;
28-
createdAt: Date | string;
29-
}
30-
3128
interface FrameworkOverviewProps {
3229
frameworkInstanceWithControls: FrameworkInstanceWithControls;
3330
tasks: (Task & { controls: Control[] })[];

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
import type { Control, Task } from '@db';
44
import { Badge, Text } from '@trycompai/design-system';
5-
import { getControlStatus } from '@/lib/control-compliance';
5+
import {
6+
type EvidenceSubmissionInfo,
7+
getControlStatus,
8+
getFrameworkAggregatePercent,
9+
} from '@/lib/control-compliance';
610
import type { FrameworkInstanceWithControls } from '@/lib/types/framework';
711

8-
interface EvidenceSubmissionInfo {
9-
id: string;
10-
formType: string;
11-
createdAt: Date | string;
12-
}
13-
1412
interface Props {
1513
framework: FrameworkInstanceWithControls;
1614
tasks: (Task & { controls: Control[] })[];
@@ -32,7 +30,7 @@ export function FrameworkProgress({ framework, tasks, evidenceSubmissions }: Pro
3230
) === 'completed',
3331
).length;
3432

35-
const percent = totalControls > 0 ? Math.round((compliantControls / totalControls) * 100) : 0;
33+
const percent = getFrameworkAggregatePercent(allControls, tasks, evidenceSubmissions);
3634
const remaining = totalControls - compliantControls;
3735

3836
const variant: 'default' | 'secondary' | 'destructive' =

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

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,27 @@ import {
1717
} from '@trycompai/design-system';
1818
import { Search } from '@trycompai/design-system/icons';
1919
import { useParams, useRouter } from 'next/navigation';
20-
import { useMemo, useState } from 'react';
20+
import { useEffect, useMemo, useState } from 'react';
21+
22+
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
2123
import type { FrameworkInstanceWithControls } from '@/lib/types/framework';
22-
import { getControlStatus } from '@/lib/control-compliance';
24+
import {
25+
type EvidenceSubmissionInfo,
26+
type RequirementArtifactCounts,
27+
getControlProgressPercent,
28+
getControlStatus,
29+
getRequirementArtifactCounts,
30+
getRequirementCompliancePercent,
31+
getRequirementStatus,
32+
} from '@/lib/control-compliance';
33+
import type { StatusType } from '@/components/status-indicator';
2334

2435
interface RequirementItem extends FrameworkEditorRequirement {
2536
mappedControlsCount: number;
2637
satisfiedControlsCount: number;
2738
compliancePercent: number;
28-
}
29-
30-
function getRequirementStatus(
31-
satisfiedCount: number,
32-
totalCount: number,
33-
): { label: string; variant: 'default' | 'secondary' | 'destructive' } {
34-
if (totalCount === 0) return { label: 'No Controls', variant: 'secondary' };
35-
if (satisfiedCount === totalCount) return { label: 'Satisfied', variant: 'default' };
36-
if (satisfiedCount > 0) return { label: 'In Progress', variant: 'secondary' };
37-
return { label: 'Not Started', variant: 'destructive' };
38-
}
39-
40-
interface EvidenceSubmissionInfo {
41-
id: string;
42-
formType: string;
43-
createdAt: Date | string;
39+
controlStatuses: StatusType[];
40+
artifactCounts: RequirementArtifactCounts;
4441
}
4542

4643
export function FrameworkRequirements({
@@ -60,6 +57,8 @@ export function FrameworkRequirements({
6057
frameworkInstanceId: string;
6158
}>();
6259
const [searchTerm, setSearchTerm] = useState('');
60+
const [page, setPage] = useState(1);
61+
const [pageSize, setPageSize] = useState(10);
6362

6463
const items = useMemo(() => {
6564
return requirementDefinitions.map((def) => {
@@ -68,26 +67,43 @@ export function FrameworkRequirements({
6867
control.requirementsMapped?.some((reqMap) => reqMap.requirementId === def.id) ?? false,
6968
);
7069

71-
const satisfiedControlsCount = mappedControls.filter(
72-
(control) => getControlStatus(
70+
const controlStatuses = mappedControls.map((control) =>
71+
getControlStatus(
7372
control.policies,
7473
tasks ?? [],
7574
control.id,
7675
control.controlDocumentTypes,
7776
evidenceSubmissions,
78-
) === 'completed',
77+
),
78+
);
79+
const satisfiedControlsCount = controlStatuses.filter(
80+
(status) => status === 'completed',
7981
).length;
8082

81-
const compliancePercent =
82-
mappedControls.length > 0
83-
? Math.round((satisfiedControlsCount / mappedControls.length) * 100)
84-
: 0;
83+
const controlProgressPercents = mappedControls.map((control) =>
84+
getControlProgressPercent(
85+
control.policies,
86+
tasks ?? [],
87+
control.id,
88+
control.controlDocumentTypes,
89+
evidenceSubmissions,
90+
),
91+
);
92+
const compliancePercent = getRequirementCompliancePercent(controlProgressPercents);
93+
94+
const artifactCounts = getRequirementArtifactCounts(
95+
mappedControls,
96+
tasks ?? [],
97+
evidenceSubmissions,
98+
);
8599

86100
return {
87101
...def,
88102
mappedControlsCount: mappedControls.length,
89103
satisfiedControlsCount,
90104
compliancePercent,
105+
controlStatuses,
106+
artifactCounts,
91107
};
92108
});
93109
}, [requirementDefinitions, frameworkInstanceWithControls.controls, tasks, evidenceSubmissions]);
@@ -103,6 +119,17 @@ export function FrameworkRequirements({
103119
);
104120
}, [items, searchTerm]);
105121

122+
const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
123+
const paginatedItems = useMemo(
124+
() => filteredItems.slice((page - 1) * pageSize, page * pageSize),
125+
[filteredItems, page, pageSize],
126+
);
127+
128+
// Snap back to page 1 when filtering or page-size changes shrink the result set.
129+
useEffect(() => {
130+
if (page > pageCount) setPage(1);
131+
}, [page, pageCount]);
132+
106133
const handleRowClick = (requirementId: string) => {
107134
router.push(`/${orgId}/frameworks/${frameworkInstanceId}/requirements/${requirementId}`);
108135
};
@@ -122,29 +149,45 @@ export function FrameworkRequirements({
122149
/>
123150
</InputGroup>
124151
</div>
125-
<Table variant="bordered">
152+
<Table
153+
variant="bordered"
154+
pagination={{
155+
page,
156+
pageCount,
157+
onPageChange: setPage,
158+
pageSize,
159+
pageSizeOptions: PAGE_SIZE_OPTIONS,
160+
onPageSizeChange: (size) => {
161+
setPageSize(size);
162+
setPage(1);
163+
},
164+
}}
165+
>
126166
<TableHeader>
127167
<TableRow>
128168
<TableHead>Identifier</TableHead>
129169
<TableHead>Name</TableHead>
130170
<TableHead>Description</TableHead>
131-
<TableHead>Controls</TableHead>
132171
<TableHead>Compliance</TableHead>
133172
<TableHead>Status</TableHead>
173+
<TableHead>Controls</TableHead>
174+
<TableHead>Policies</TableHead>
175+
<TableHead>Tasks</TableHead>
176+
<TableHead>Documents</TableHead>
134177
</TableRow>
135178
</TableHeader>
136179
<TableBody>
137-
{filteredItems.length === 0 ? (
180+
{paginatedItems.length === 0 ? (
138181
<TableRow>
139-
<TableCell colSpan={6}>
182+
<TableCell colSpan={9}>
140183
<Text size="sm" variant="muted">
141184
No requirements found.
142185
</Text>
143186
</TableCell>
144187
</TableRow>
145188
) : (
146-
filteredItems.map((item) => {
147-
const status = getRequirementStatus(item.satisfiedControlsCount, item.mappedControlsCount);
189+
paginatedItems.map((item) => {
190+
const status = getRequirementStatus(item.controlStatuses);
148191
const identifier = item.identifier?.trim();
149192

150193
return (
@@ -174,19 +217,12 @@ export function FrameworkRequirements({
174217
</TableCell>
175218
<TableCell>
176219
<span
177-
className="block max-w-[420px] truncate text-sm"
220+
className="block max-w-[240px] truncate text-sm"
178221
title={item.description || ''}
179222
>
180223
{item.description || '—'}
181224
</span>
182225
</TableCell>
183-
<TableCell>
184-
<div className="tabular-nums">
185-
<Text size="sm" variant="muted">
186-
{item.satisfiedControlsCount}/{item.mappedControlsCount}
187-
</Text>
188-
</div>
189-
</TableCell>
190226
<TableCell>
191227
<div className="flex items-center gap-2 min-w-[100px]">
192228
<div className="flex-1 rounded-full bg-muted/50 h-1.5">
@@ -205,6 +241,34 @@ export function FrameworkRequirements({
205241
<TableCell>
206242
<Badge variant={status.variant}>{status.label}</Badge>
207243
</TableCell>
244+
<TableCell>
245+
<div className="tabular-nums">
246+
<Text size="sm" variant="muted">
247+
{item.satisfiedControlsCount}/{item.mappedControlsCount}
248+
</Text>
249+
</div>
250+
</TableCell>
251+
<TableCell>
252+
<div className="tabular-nums">
253+
<Text size="sm" variant="muted">
254+
{item.artifactCounts.policies.completed}/{item.artifactCounts.policies.total}
255+
</Text>
256+
</div>
257+
</TableCell>
258+
<TableCell>
259+
<div className="tabular-nums">
260+
<Text size="sm" variant="muted">
261+
{item.artifactCounts.tasks.completed}/{item.artifactCounts.tasks.total}
262+
</Text>
263+
</div>
264+
</TableCell>
265+
<TableCell>
266+
<div className="tabular-nums">
267+
<Text size="sm" variant="muted">
268+
{item.artifactCounts.documents.completed}/{item.artifactCounts.documents.total}
269+
</Text>
270+
</div>
271+
</TableCell>
208272
</TableRow>
209273
);
210274
})

apps/app/src/app/(app)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/RequirementControls.tsx

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
'use client';
22

33
import type { Control, RequirementMap, Task } from '@db';
4-
import { Heading } from '@trycompai/design-system';
4+
import { Badge, Heading, Text } from '@trycompai/design-system';
5+
import {
6+
type EvidenceSubmissionInfo,
7+
getControlStatus,
8+
getFrameworkAggregatePercent,
9+
} from '@/lib/control-compliance';
510
import { RequirementControlsTable } from './table/RequirementControlsTable';
611

7-
interface EvidenceSubmissionInfo {
8-
id: string;
9-
formType: string;
10-
createdAt: Date | string;
11-
}
12+
type ControlWithRelations = Control & {
13+
policies?: Array<{ id: string; name: string; status: string }>;
14+
controlDocumentTypes?: Array<{ formType: string }>;
15+
};
1216

1317
interface RequirementControlsProps {
1418
tasks: (Task & { controls: Control[] })[];
15-
relatedControls: (RequirementMap & { control: Control & { policies: Array<{ id: string; name: string; status: string }> } } )[];
19+
relatedControls: (RequirementMap & { control: ControlWithRelations })[];
1620
evidenceSubmissions?: EvidenceSubmissionInfo[];
1721
frameworkInstanceId: string;
1822
}
@@ -23,17 +27,57 @@ export function RequirementControls({
2327
evidenceSubmissions = [],
2428
frameworkInstanceId,
2529
}: RequirementControlsProps) {
30+
const controls = relatedControls.map((rc) => rc.control);
31+
const totalControls = controls.length;
32+
const compliantControls = controls.filter(
33+
(control) =>
34+
getControlStatus(
35+
control.policies ?? [],
36+
tasks,
37+
control.id,
38+
control.controlDocumentTypes,
39+
evidenceSubmissions,
40+
) === 'completed',
41+
).length;
42+
const remaining = totalControls - compliantControls;
43+
const percent = getFrameworkAggregatePercent(controls, tasks, evidenceSubmissions);
44+
const variant: 'default' | 'secondary' | 'destructive' =
45+
percent >= 80 ? 'default' : percent >= 60 ? 'secondary' : 'destructive';
46+
2647
return (
2748
<div className="space-y-4">
49+
{totalControls > 0 && (
50+
<div className="flex flex-col gap-4">
51+
<div className="flex flex-wrap items-center gap-6 text-sm">
52+
<Badge variant={variant}>{percent}% compliant</Badge>
53+
<Text size="sm" variant="muted">
54+
{compliantControls} completed
55+
</Text>
56+
<Text size="sm" variant="muted">
57+
{remaining} remaining
58+
</Text>
59+
<Text size="sm" variant="muted">
60+
{totalControls} total controls
61+
</Text>
62+
</div>
63+
<div className="h-2 w-full rounded-full bg-muted/50">
64+
<div
65+
className="h-full rounded-full bg-primary transition-all duration-300"
66+
style={{ width: `${percent}%` }}
67+
/>
68+
</div>
69+
</div>
70+
)}
71+
2872
<div className="flex items-center gap-2">
2973
<Heading level="3">Controls</Heading>
3074
<span className="text-muted-foreground bg-muted/50 rounded-xs px-2 py-1 text-xs tabular-nums">
31-
{relatedControls.length}
75+
{totalControls}
3276
</span>
3377
</div>
3478

3579
<RequirementControlsTable
36-
controls={relatedControls.map((rc) => rc.control)}
80+
controls={controls}
3781
tasks={tasks}
3882
evidenceSubmissions={evidenceSubmissions}
3983
frameworkInstanceId={frameworkInstanceId}

0 commit comments

Comments
 (0)