@@ -7,18 +7,27 @@ interface Props {
77 onChanged ?: ( ) => void ;
88}
99
10+ // cache 5 minutes
11+ const CACHE_TTL_MS = 5 * 60 * 1000 ;
12+
13+ // styles
1014const wrap : React . CSSProperties = { display : 'grid' , gap : 12 } ;
1115const row : React . CSSProperties = { display : 'flex' , alignItems : 'center' , gap : 8 } ;
12- const table : React . CSSProperties = { width : '100%' , borderCollapse : 'collapse' } ;
13- const thtd : React . CSSProperties = { border : '1px solid #e9ecef' , padding : '8px' , fontSize : 13 } ;
16+ const table : React . CSSProperties = { width : '100%' , borderCollapse : 'collapse' , userSelect : 'text' } ;
17+ const thtdBase : React . CSSProperties = { border : '1px solid #e9ecef' , padding : '8px' , fontSize : 13 } ;
18+ const th : React . CSSProperties = { ...thtdBase , fontWeight : 700 , textAlign : 'left' } ;
19+ const td : React . CSSProperties = { ...thtdBase , userSelect : 'text' } ; // ← allow highlighting
20+ const tdCenter : React . CSSProperties = { ...td , textAlign : 'center' , userSelect : 'none' } ; // actions
1421const btn : React . CSSProperties = { padding : '6px 10px' , borderRadius : 6 , cursor : 'pointer' , fontWeight : 600 } ;
1522const dangerBtn : React . CSSProperties = { ...btn , border : '1px solid #ffc9c9' , background : '#fff5f5' , color : '#e03131' } ;
1623const primaryBtn : React . CSSProperties = { ...btn , border : 'none' , background : '#3498db' , color : '#fff' } ;
1724const input : React . CSSProperties = { padding : '8px 10px' , borderRadius : 6 , border : '1px solid #dee2e6' , fontSize : 13 , flex : 1 } ;
1825
19- // role helpers (non-hooks; ok at top level)
20- const isOwnerRole = ( role ?: string ) => role === 'OWNER' ;
21- const prettyRole = ( role ?: string ) => role === 'OWNER' ? 'Owner' : 'Member' ;
26+ // role helpers – use canonical `role` first, fallback to `roleEn`
27+ const isOwnerRole = ( role ?: string , roleEn ?: string ) =>
28+ role === 'OWNER' || ( ! ! roleEn && roleEn . toLowerCase ( ) . includes ( 'owner' ) ) ;
29+ const prettyRole = ( role ?: string , roleEn ?: string ) =>
30+ isOwnerRole ( role , roleEn ) ? 'Owner' : 'Member' ;
2231
2332export const VsumUsersTab : React . FC < Props > = ( { vsumId, onChanged } ) => {
2433 const [ members , setMembers ] = useState < VsumUserResponse [ ] > ( [ ] ) ;
@@ -31,40 +40,39 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
3140 const [ searchResults , setSearchResults ] = useState < UserSearchItem [ ] > ( [ ] ) ;
3241 const [ selectedUser , setSelectedUser ] = useState < UserSearchItem | null > ( null ) ;
3342
34- // --- caching objects MUST be inside the component (hooks rule) ---
43+ // refs & cache
3544 const searchTimer = useRef < number | undefined > ( undefined ) ;
3645 const usersCacheRef = useRef < { at : number ; data : UserSearchItem [ ] } | null > ( null ) ;
37- const CACHE_TTL_MS = 30_000 ;
3846
39- // ---------- Fetch members ----------
47+ // fetch members
4048 const fetchMembers = useCallback ( async ( ) => {
4149 setErr ( '' ) ;
4250 setLoading ( true ) ;
4351 try {
44- const res = await apiService . getVsumMembers ( vsumId ) ; // [FETCH_MEMBERS]
52+ const res = await apiService . getVsumMembers ( vsumId ) ; // [FETCH_MEMBERS]
4553 setMembers ( res . data || [ ] ) ;
4654 } catch ( e : any ) {
4755 setErr ( e ?. message || 'Failed to load members' ) ;
4856 } finally {
4957 setLoading ( false ) ;
5058 }
51- } , [ vsumId , CACHE_TTL_MS ] ) ;
59+ } , [ vsumId ] ) ;
5260
5361 useEffect ( ( ) => { fetchMembers ( ) ; } , [ fetchMembers ] ) ;
5462
55- // ---------- Load users for search (cached with TTL) ----------
63+ // cached all- users list for typeahead
5664 const loadUsersForSearch = useCallback ( async ( ) => {
5765 const now = Date . now ( ) ;
5866 if ( usersCacheRef . current && now - usersCacheRef . current . at < CACHE_TTL_MS ) {
5967 return usersCacheRef . current . data ;
6068 }
61- const res = await apiService . searchUsers ( { pageNumber : 0 , pageSize : 200 } ) ; // cached backend list
69+ const res = await apiService . searchUsers ( { pageNumber : 0 , pageSize : 200 } ) ;
6270 const data = res . data || [ ] ;
6371 usersCacheRef . current = { at : now , data } ;
6472 return data ;
6573 } , [ ] ) ;
6674
67- // ---------- Debounced user search ( client-side filtering + cache) ----------
75+ // debounced client-side filter
6876 useEffect ( ( ) => {
6977 if ( searchTimer . current ) window . clearTimeout ( searchTimer . current ) ;
7078
@@ -79,7 +87,7 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
7987 searchTimer . current = window . setTimeout ( async ( ) => {
8088 try {
8189 setErr ( '' ) ;
82- const all = await loadUsersForSearch ( ) ; // uses cache + TTL
90+ const all = await loadUsersForSearch ( ) ;
8391 const filtered = all . filter ( u => {
8492 const name = [ u . firstName , u . lastName ] . filter ( Boolean ) . join ( ' ' ) . toLowerCase ( ) ;
8593 const email = ( u . email || '' ) . toLowerCase ( ) ;
@@ -98,7 +106,7 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
98106 } ;
99107 } , [ query , loadUsersForSearch ] ) ;
100108
101- // ---------- Add member (server defaults to MEMBER) ----------
109+ // add member
102110 const addMember = async ( ) => {
103111 if ( ! selectedUser ) return ;
104112 try {
@@ -114,12 +122,12 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
114122 }
115123 } ;
116124
117- // ---------- Remove member (only for MEMBER) ----------
125+ // remove member (only non-owner get a button)
118126 const removeMember = async ( vsumUserId : number ) => {
119127 if ( ! window . confirm ( 'Remove this member from the vSUM?' ) ) return ;
120128 try {
121129 setErr ( '' ) ;
122- await apiService . removeVsumMember ( vsumUserId ) ; // [REMOVE_MEMBER]
130+ await apiService . removeVsumMember ( vsumUserId ) ; // [REMOVE_MEMBER]
123131 await fetchMembers ( ) ;
124132 onChanged ?.( ) ;
125133 } catch ( e : any ) {
@@ -142,7 +150,7 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
142150 </ div >
143151 ) }
144152
145- { /* Add member (no role selector) */ }
153+ { /* Add member */ }
146154 < div style = { { ...row , flexWrap : 'wrap' } } >
147155 < input
148156 placeholder = "Search user by name or email…"
@@ -197,32 +205,32 @@ export const VsumUsersTab: React.FC<Props> = ({ vsumId, onChanged }) => {
197205 </ div >
198206 ) }
199207
200- { /* Members table */ }
208+ { /* Members table (text is selectable) */ }
201209 < div style = { { marginTop : 8 } } >
202210 < table style = { table } >
203211 < thead >
204212 < tr >
205- < th style = { { ... thtd , textAlign : 'left' } } > Name</ th >
206- < th style = { { ... thtd , textAlign : 'left' } } > Email</ th >
207- < th style = { { ... thtd , textAlign : 'left' } } > Role</ th >
208- < th style = { { ...thtd , width : 120 } } > Actions</ th >
213+ < th style = { th } > Name</ th >
214+ < th style = { th } > Email</ th >
215+ < th style = { th } > Role</ th >
216+ < th style = { { ...th , width : 120 , textAlign : 'center' } } > Actions</ th >
209217 </ tr >
210218 </ thead >
211219 < tbody >
212220 { loading ? (
213- < tr > < td colSpan = { 4 } style = { { ...thtd , fontStyle : 'italic' , color : '#6c757d' } } > Loading…</ td > </ tr >
221+ < tr > < td colSpan = { 4 } style = { { ...td , fontStyle : 'italic' , color : '#6c757d' } } > Loading…</ td > </ tr >
214222 ) : members . length === 0 ? (
215- < tr > < td colSpan = { 4 } style = { { ...thtd , fontStyle : 'italic' , color : '#6c757d' } } > No members yet.</ td > </ tr >
223+ < tr > < td colSpan = { 4 } style = { { ...td , fontStyle : 'italic' , color : '#6c757d' } } > No members yet.</ td > </ tr >
216224 ) : (
217225 members . map ( m => {
218226 const fullName = [ m . firstName , m . lastName ] . filter ( Boolean ) . join ( ' ' ) || '—' ;
219- const owner = isOwnerRole ( m . role ) ;
227+ const owner = isOwnerRole ( m . role , ( m as any ) . roleEn ) ;
220228 return (
221229 < tr key = { m . id } >
222- < td style = { thtd } > { fullName } </ td >
223- < td style = { thtd } > { m . email } </ td >
224- < td style = { thtd } > { prettyRole ( m . role ) } </ td >
225- < td style = { { ... thtd , textAlign : 'center' } } >
230+ < td style = { td } title = { fullName } > { fullName } </ td >
231+ < td style = { td } title = { m . email } > { m . email } </ td >
232+ < td style = { td } > { prettyRole ( m . role , ( m as any ) . roleEn ) } </ td >
233+ < td style = { tdCenter } >
226234 { ! owner && (
227235 < button style = { dangerBtn } onClick = { ( ) => removeMember ( m . id ) } >
228236 Remove
0 commit comments