11/* eslint-disable react/destructuring-assignment */
22import './Team.css' ;
3- import { connect , useSelector } from 'react-redux' ;
3+ import React , { useEffect , useState } from 'react' ;
4+ import PropTypes from 'prop-types' ;
5+ import { connect , useSelector , useDispatch } from 'react-redux' ;
46import { Button } from 'reactstrap' ;
57import hasPermission from '~/utils/permissions' ;
68import { boxStyle , boxStyleDark } from '~/styles' ;
79import { DELETE } from '../../languages/en/ui' ;
10+ import { getTeamMembers } from '../../actions/allTeamsAction' ;
11+ import { fetchTeamMembersCached , getCachedTeamMembers } from './teamMembersCache' ;
12+
13+ function computeCounts ( members , loading , localMembers ) {
14+ const list = Array . isArray ( members ) ? members : [ ] ;
15+
16+ if ( loading && localMembers == null ) return { total : '…' , active : '…' , inactive : '…' } ;
17+ const tot = list . length ;
18+ if ( tot === 0 ) return { total : 0 , active : 0 , inactive : 0 } ;
19+
20+ let act = 0 ;
21+ let sawFlag = false ;
22+ for ( const m of list ) {
23+ const flag = m ?. isActive ?? m ?. active ;
24+ if ( typeof flag === 'boolean' ) {
25+ sawFlag = true ;
26+ if ( flag ) act += 1 ;
27+ }
28+ }
29+ if ( ! sawFlag ) return { total : tot , active : '…' , inactive : '…' } ;
30+ return { total : tot , active : act , inactive : tot - act } ;
31+ }
832import headerStyles from './TeamTableHeader.module.css' ;
933
1034export function Team ( props ) {
11- const darkMode = useSelector ( state => state . theme . darkMode ) ;
35+ const dispatch = useDispatch ( ) ;
36+ const darkMode = useSelector ( s => s . theme . darkMode ) ;
1237 const canDeleteTeam = props . hasPermission ( 'deleteTeam' ) ;
1338 const canPutTeam = props . hasPermission ( 'putTeam' ) ;
1439
40+ // Keep a raw id for callbacks (number stays number in tests)
41+ const teamIdRaw =
42+ typeof props . teamId === 'string' && / ^ \d + $ / . test ( props . teamId )
43+ ? Number ( props . teamId )
44+ : props . teamId ;
45+
46+ // string key for cache/DOM ids
47+ const teamIdKey = String ( props . teamId ?? '' ) ;
48+
49+ const [ localMembers , setLocalMembers ] = useState ( ( ) => getCachedTeamMembers ( teamIdKey ) || null ) ;
50+ const [ loading , setLoading ] = useState ( false ) ;
51+
52+ useEffect ( ( ) => {
53+ const cached = getCachedTeamMembers ( teamIdKey ) ;
54+ if ( cached && ! localMembers ) setLocalMembers ( cached ) ;
55+ // eslint-disable-next-line react-hooks/exhaustive-deps
56+ } , [ teamIdKey ] ) ;
57+
58+ useEffect ( ( ) => {
59+ if ( ! getCachedTeamMembers ( teamIdKey ) && teamIdKey ) {
60+ fetchTeamMembersCached ( dispatch , getTeamMembers , teamIdKey )
61+ . then ( setLocalMembers )
62+ . catch ( ( ) => { } ) ;
63+ }
64+ } , [ dispatch , teamIdKey ] ) ;
65+
66+ const members = localMembers ?? props . team ?. members ?? [ ] ;
67+ const { total, active, inactive } = computeCounts ( members , loading , localMembers ) ;
68+
69+ // Fire callback immediately (keeps tests & UX snappy), then refresh members
70+ const handleOpenMembers = ( ) => {
71+ if ( typeof props . onMembersClick === 'function' ) {
72+ props . onMembersClick ( teamIdRaw , props . name , props . teamCode ) ;
73+ }
74+
75+ setLoading ( true ) ;
76+ fetchTeamMembersCached ( dispatch , getTeamMembers , teamIdKey )
77+ . then ( setLocalMembers )
78+ . catch ( ( ) => { } )
79+ . finally ( ( ) => setLoading ( false ) ) ;
80+ } ;
81+
1582 return (
16- < tr className = "teams__tr" id = { `tr_${ props . teamId } ` } >
83+ < tr className = "teams__tr" id = { `tr_${ teamIdKey } ` } >
1784 < th className = "teams__order--input" scope = "row" >
1885 < div > { ( props . index ?? 0 ) + 1 } </ div >
1986 </ th >
2087 { /* Wrap long names vertically */ }
21- < td className = { headerStyles . teamNameCol } > { props . name } </ td >
88+ < td className = { headerStyles . teamNameCol } >
89+ { props . name } ({ total } | { active } | { inactive } )
90+ </ td >
2291 < td className = "teams__active--input" >
2392 < button
2493 data-testid = "active-marker"
2594 type = "button"
2695 onClick = { ( ) => {
2796 if ( canDeleteTeam || canPutTeam ) {
28- props . onStatusClick ( props . name , props . teamId , props . active , props . teamCode ) ;
97+ props . onStatusClick ( props . name , teamIdRaw , props . active , props . teamCode ) ;
2998 }
3099 } }
31- style = { {
32- boxStyle,
33- } }
100+ // style={boxStyle}
34101 aria-label = { `Change status for team ${ props . name } ` }
35102 >
36103 < div className = { props . active ? 'isActive' : 'isNotActive' } >
37- < i className = "fa fa-circle" aria-hidden = "true" > </ i >
104+ < i className = "fa fa-circle" aria-hidden = "true" / >
38105 </ div >
39106 </ button >
40107 </ td >
108+
41109 < td className = "centered-cell" >
42110 < button
43111 style = { darkMode ? { } : boxStyle }
44112 type = "button"
45113 className = "btn btn-outline-info"
46- onClick = { ( ) => {
47- props . onMembersClick ( props . teamId , props . name , props . teamCode ) ;
114+ onMouseEnter = { ( ) => {
115+ if ( ! getCachedTeamMembers ( teamIdKey ) ) {
116+ fetchTeamMembersCached ( dispatch , getTeamMembers , teamIdKey )
117+ . then ( setLocalMembers )
118+ . catch ( ( ) => { } ) ;
119+ }
48120 } }
121+ onClick = { handleOpenMembers }
49122 data-testid = "members-btn"
50123 aria-label = "Users"
124+ disabled = { loading }
51125 >
52- < i className = "fa fa-users" aria-hidden = "true " />
126+ { loading ? < i className = "fa fa-spinner fa-spin" /> : < i className = "fa fa-users " /> }
53127 </ button >
54128 </ td >
129+
55130 { ( canDeleteTeam || canPutTeam ) && (
56131 < td >
57132 < span className = "usermanagement-actions-cell" >
58133 < Button
59134 color = "success"
60- // className="btn btn-outline-success"
61- onClick = { ( ) => {
62- props . onEditTeam ( props . name , props . teamId , props . active , props . teamCode ) ;
63- } }
135+ onClick = { ( ) => props . onEditTeam ( props . name , teamIdRaw , props . active , props . teamCode ) }
64136 style = { darkMode ? { } : boxStyle }
65137 disabled = { ! canPutTeam }
66138 >
@@ -70,10 +142,9 @@ export function Team(props) {
70142 < span className = "usermanagement-actions-cell" >
71143 < Button
72144 color = "danger"
73- // className="btn btn-outline-danger"
74- onClick = { ( ) => {
75- props . onDeleteClick ( props . name , props . teamId , props . active , props . teamCode ) ;
76- } }
145+ onClick = { ( ) =>
146+ props . onDeleteClick ( props . name , teamIdRaw , props . active , props . teamCode )
147+ }
77148 style = { darkMode ? boxStyleDark : boxStyle }
78149 disabled = { ! canDeleteTeam }
79150 >
@@ -85,4 +156,58 @@ export function Team(props) {
85156 </ tr >
86157 ) ;
87158}
159+
160+ Team . propTypes = {
161+ // injected by connect
162+ hasPermission : PropTypes . func . isRequired ,
163+
164+ // basic identity/labels
165+ teamId : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] ) . isRequired ,
166+ name : PropTypes . string . isRequired ,
167+ teamCode : PropTypes . string ,
168+ index : PropTypes . number ,
169+
170+ // status flags
171+ active : PropTypes . bool ,
172+
173+ // callbacks
174+ onMembersClick : PropTypes . func ,
175+ onStatusClick : PropTypes . func ,
176+ onEditTeam : PropTypes . func ,
177+ onDeleteClick : PropTypes . func ,
178+
179+ // optional team object (used for members & modifiedDatetime)
180+ team : PropTypes . shape ( {
181+ _id : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] ) ,
182+ teamName : PropTypes . string ,
183+ teamCode : PropTypes . string ,
184+ isActive : PropTypes . bool ,
185+ members : PropTypes . arrayOf (
186+ PropTypes . shape ( {
187+ _id : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] ) ,
188+ firstName : PropTypes . string ,
189+ lastName : PropTypes . string ,
190+ isActive : PropTypes . bool ,
191+ active : PropTypes . bool ,
192+ } ) ,
193+ ) ,
194+ modifiedDatetime : PropTypes . oneOfType ( [
195+ PropTypes . string ,
196+ PropTypes . number ,
197+ PropTypes . instanceOf ( Date ) ,
198+ ] ) ,
199+ } ) ,
200+ } ;
201+
202+ Team . defaultProps = {
203+ teamCode : '' ,
204+ index : 0 ,
205+ active : false ,
206+ onMembersClick : undefined ,
207+ onStatusClick : undefined ,
208+ onEditTeam : undefined ,
209+ onDeleteClick : undefined ,
210+ team : undefined ,
211+ } ;
212+
88213export default connect ( null , { hasPermission } ) ( Team ) ;
0 commit comments