@@ -17,30 +17,27 @@ import {
1717} from '@trycompai/design-system' ;
1818import { Search } from '@trycompai/design-system/icons' ;
1919import { 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 ] ;
2123import 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
2435interface 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
4643export 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 } )
0 commit comments