@@ -9,18 +9,9 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight =
99 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
1010 const [ expandedRows , setExpandedRows ] = useState ( { } ) ;
1111 const itemsPerPage = 20 ;
12- const totalPages = Math . ceil ( changeLogs . length / itemsPerPage ) ;
13- const indexOfLastItem = currentPage * itemsPerPage ;
14- const indexOfFirstItem = indexOfLastItem - itemsPerPage ;
15- const currentItems = changeLogs . slice ( indexOfFirstItem , indexOfLastItem ) ;
1612 const fontColor = darkMode ? 'text-light' : '' ;
1713 const bgYinmnBlue = darkMode ? 'bg-yinmn-blue' : '' ;
1814 const addDark = darkMode ? '-dark' : '' ;
19- const paginate = pageNumber => {
20- if ( pageNumber > 0 && pageNumber <= totalPages ) {
21- setCurrentPage ( pageNumber ) ;
22- }
23- } ;
2415
2516 const normalize = v =>
2617 ( v ?? '' )
@@ -36,6 +27,117 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight =
3627 return name ;
3728 } ;
3829
30+ // Group logs by name first, then by editor and time within each name group
31+ const groupLogsByNameThenEditorAndTime = logs => {
32+ const nameGroups = [ ] ;
33+ const TIME_TOLERANCE_MS = 2000 ; // 2 seconds tolerance for "same time"
34+
35+ logs . forEach ( log => {
36+ // Get the target name (person whose permissions are being changed)
37+ const targetName = log ?. individualName ? formatName ( log . individualName ) : log . roleName || '' ;
38+ const normalizedTargetName = normalize ( targetName ) ;
39+
40+ // Find existing name group
41+ let foundNameGroup = null ;
42+ for ( let i = nameGroups . length - 1 ; i >= 0 ; i -- ) {
43+ const nameGroup = nameGroups [ i ] ;
44+ if ( nameGroup . normalizedTargetName === normalizedTargetName ) {
45+ foundNameGroup = nameGroup ;
46+ break ;
47+ }
48+ }
49+
50+ if ( foundNameGroup ) {
51+ // Within the same name group, group by editor and time
52+ const logTime = new Date ( log . logDateTime ) . getTime ( ) ;
53+ const editorKey = `${ log . requestorEmail || '' } _${ log . requestorRole || '' } ` ;
54+
55+ // Find existing editor-time sub-group
56+ let foundSubGroup = null ;
57+ for ( let i = foundNameGroup . subGroups . length - 1 ; i >= 0 ; i -- ) {
58+ const subGroup = foundNameGroup . subGroups [ i ] ;
59+ const subGroupTime = new Date ( subGroup . logs [ 0 ] . logDateTime ) . getTime ( ) ;
60+ const firstLog = subGroup . logs [ 0 ] ;
61+ const subGroupEditorEmail = firstLog . requestorEmail || '' ;
62+ const subGroupEditorRole = firstLog . requestorRole || '' ;
63+ const subGroupEditorKey = `${ subGroupEditorEmail } _${ subGroupEditorRole } ` ;
64+
65+ if (
66+ subGroupEditorKey === editorKey &&
67+ Math . abs ( logTime - subGroupTime ) <= TIME_TOLERANCE_MS
68+ ) {
69+ foundSubGroup = subGroup ;
70+ break ;
71+ }
72+ }
73+
74+ if ( foundSubGroup ) {
75+ foundSubGroup . logs . push ( log ) ;
76+ } else {
77+ foundNameGroup . subGroups . push ( {
78+ logs : [ log ] ,
79+ subGroupId : `subgroup_${ foundNameGroup . subGroups . length } _${ log . _id } ` ,
80+ } ) ;
81+ }
82+ foundNameGroup . logs . push ( log ) ;
83+ } else {
84+ // Create new name group
85+ nameGroups . push ( {
86+ targetName,
87+ normalizedTargetName,
88+ logs : [ log ] ,
89+ subGroups : [
90+ {
91+ logs : [ log ] ,
92+ subGroupId : `subgroup_0_${ log . _id } ` ,
93+ } ,
94+ ] ,
95+ groupId : `namegroup_${ nameGroups . length } _${ log . _id } ` ,
96+ } ) ;
97+ }
98+ } ) ;
99+
100+ return nameGroups ;
101+ } ;
102+
103+ // Flatten grouped logs for pagination while preserving grouping info
104+ const nameGroupedLogs = groupLogsByNameThenEditorAndTime ( changeLogs ) ;
105+ const flattenedLogs = [ ] ;
106+ nameGroupedLogs . forEach ( nameGroup => {
107+ nameGroup . logs . forEach ( ( log , nameIndex ) => {
108+ // Find which sub-group (editor-time group) this log belongs to
109+ const subGroup = nameGroup . subGroups . find ( sg => sg . logs . some ( sgLog => sgLog . _id === log . _id ) ) ;
110+ const subGroupIndex = subGroup ? subGroup . logs . findIndex ( sgLog => sgLog . _id === log . _id ) : 0 ;
111+ const isFirstInSubGroup = subGroup ? subGroupIndex === 0 : nameIndex === 0 ;
112+ const isSubGrouped = subGroup ? subGroup . logs . length > 1 : false ;
113+
114+ flattenedLogs . push ( {
115+ ...log ,
116+ isNameGrouped : nameGroup . logs . length > 1 ,
117+ nameGroupId : nameGroup . groupId ,
118+ nameGroupIndex : nameIndex ,
119+ nameGroupSize : nameGroup . logs . length ,
120+ isFirstInNameGroup : nameIndex === 0 ,
121+ isSubGrouped,
122+ subGroupId : subGroup ?. subGroupId ,
123+ subGroupIndex,
124+ subGroupSize : subGroup ?. logs . length || 1 ,
125+ isFirstInSubGroup,
126+ } ) ;
127+ } ) ;
128+ } ) ;
129+
130+ const totalPages = Math . ceil ( flattenedLogs . length / itemsPerPage ) ;
131+ const indexOfLastItem = currentPage * itemsPerPage ;
132+ const indexOfFirstItem = indexOfLastItem - itemsPerPage ;
133+ const currentItems = flattenedLogs . slice ( indexOfFirstItem , indexOfLastItem ) ;
134+
135+ const paginate = pageNumber => {
136+ if ( pageNumber > 0 && pageNumber <= totalPages ) {
137+ setCurrentPage ( pageNumber ) ;
138+ }
139+ } ;
140+
39141 const renderPageNumbers = ( ) => {
40142 const pageNumbers = [ ] ;
41143 const maxPageNumbersToShow = 5 ;
@@ -179,23 +281,41 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight =
179281 const nameValue = log ?. individualName ? formatName ( log . individualName ) : log . roleName ;
180282
181283 const shouldHighlight = roleSet . has ( normalize ( nameValue ) ) ;
284+ // Rowspan for name column - spans all entries with same target name
285+ const nameRowSpan =
286+ log . isNameGrouped && log . isFirstInNameGroup ? log . nameGroupSize : undefined ;
287+ // Rowspan for date/time, editor role, editor email - spans entries with same editor and time
288+ const subGroupRowSpan =
289+ log . isSubGrouped && log . isFirstInSubGroup ? log . subGroupSize : undefined ;
182290
183291 return (
184292 < tr key = { log . _id } className = { shouldHighlight ? styles . highlightRow : '' } >
185- < td className = { styles . permissionChangeLogTableCell } >
186- { `${ formatDate ( log . logDateTime ) } ${ formattedAmPmTime ( log . logDateTime ) } ` }
187- </ td >
293+ { /* Date/Time column - only show for first row in sub-group, or if not sub-grouped */ }
294+ { log . isFirstInSubGroup || ! log . isSubGrouped ? (
295+ < td
296+ className = { `${ styles . permissionChangeLogTableCell } ${ bgYinmnBlue } ` . trim ( ) }
297+ { ...( subGroupRowSpan && { rowSpan : subGroupRowSpan } ) }
298+ style = { { verticalAlign : 'top' } }
299+ >
300+ { `${ formatDate ( log . logDateTime ) } ${ formattedAmPmTime ( log . logDateTime ) } ` }
301+ </ td >
302+ ) : null }
188303
189- < td
190- className = { styles . permissionChangeLogTableCell }
191- style = { {
192- fontWeight : log ?. individualName ? 'bold' : 'normal' ,
193- } }
194- >
195- { log ?. individualName ? formatName ( log . individualName ) : log . roleName }
196- </ td >
304+ { /* Name column - only show for first row in name group, or if not name-grouped */ }
305+ { log . isFirstInNameGroup || ! log . isNameGrouped ? (
306+ < td
307+ className = { `${ styles . permissionChangeLogTableCell } ${ bgYinmnBlue } ` . trim ( ) }
308+ { ...( nameRowSpan && { rowSpan : nameRowSpan } ) }
309+ style = { {
310+ fontWeight : log ?. individualName ? 'bold' : 'normal' ,
311+ verticalAlign : 'top' ,
312+ } }
313+ >
314+ { log ?. individualName ? formatName ( log . individualName ) : log . roleName }
315+ </ td >
316+ ) : null }
197317
198- < td className = { styles . permissionChangeLogTableCell } >
318+ < td className = { ` ${ styles . permissionChangeLogTableCell } ${ bgYinmnBlue } ` . trim ( ) } >
199319 { renderPermissions ( log . permissions , log . _id ) }
200320 </ td >
201321
@@ -207,9 +327,27 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight =
207327 { renderPermissions ( log . permissionsRemoved , `${ log . _id } _removed` ) }
208328 </ td >
209329
210- < td className = { styles . permissionChangeLogTableCell } > { log . requestorRole } </ td >
330+ { /* Editor Role column - only show for first row in sub-group, or if not sub-grouped */ }
331+ { log . isFirstInSubGroup || ! log . isSubGrouped ? (
332+ < td
333+ className = { `${ styles . permissionChangeLogTableCell } ${ bgYinmnBlue } ` . trim ( ) }
334+ { ...( subGroupRowSpan && { rowSpan : subGroupRowSpan } ) }
335+ style = { { verticalAlign : 'top' } }
336+ >
337+ { log . requestorRole }
338+ </ td >
339+ ) : null }
211340
212- < td className = { styles . permissionChangeLogTableCell } > { log . requestorEmail } </ td >
341+ { /* Editor Email column - only show for first row in sub-group, or if not sub-grouped */ }
342+ { log . isFirstInSubGroup || ! log . isSubGrouped ? (
343+ < td
344+ className = { `${ styles . permissionChangeLogTableCell } ${ bgYinmnBlue } ` . trim ( ) }
345+ { ...( subGroupRowSpan && { rowSpan : subGroupRowSpan } ) }
346+ style = { { verticalAlign : 'top' } }
347+ >
348+ { log . requestorEmail }
349+ </ td >
350+ ) : null }
213351 </ tr >
214352 ) ;
215353 } ) }
0 commit comments