Skip to content

Commit d8ed1be

Browse files
committed
fix bug error
1 parent 898772a commit d8ed1be

File tree

1 file changed

+38
-30
lines changed

1 file changed

+38
-30
lines changed

src/components/ui/VsumUsersTab.tsx

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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
1014
const wrap: React.CSSProperties = { display: 'grid', gap: 12 };
1115
const 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
1421
const btn: React.CSSProperties = { padding: '6px 10px', borderRadius: 6, cursor: 'pointer', fontWeight: 600 };
1522
const dangerBtn: React.CSSProperties = { ...btn, border: '1px solid #ffc9c9', background: '#fff5f5', color: '#e03131' };
1623
const primaryBtn: React.CSSProperties = { ...btn, border: 'none', background: '#3498db', color: '#fff' };
1724
const 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

2332
export 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

Comments
 (0)