Skip to content

Commit f40768b

Browse files
authored
Merge pull request #325 from con2/CON2-217-Admin-Users-from-modals-to-page
Con2 217 admin users from modals to page
2 parents e8634c6 + 2297976 commit f40768b

File tree

18 files changed

+2010
-1523
lines changed

18 files changed

+2010
-1523
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import { useState, useEffect, useCallback } from "react";
2+
import { useAppDispatch, useAppSelector } from "@/store/hooks";
3+
import { useLanguage } from "@/context/LanguageContext";
4+
import { t } from "@/translations";
5+
import { toast } from "sonner";
6+
import { Button } from "@/components/ui/button";
7+
import {
8+
Select,
9+
SelectContent,
10+
SelectItem,
11+
SelectTrigger,
12+
SelectValue,
13+
} from "@/components/ui/select";
14+
import { Label } from "@/components/ui/label";
15+
import { Textarea } from "@/components/ui/textarea";
16+
import {
17+
unbanUser,
18+
selectUserBanningLoading,
19+
} from "@/store/slices/userBanningSlice";
20+
import { UserProfile } from "@common/user.types";
21+
import { BanType, SimpleBanHistoryItem } from "@/types/userBanning";
22+
import { useRoles } from "@/hooks/useRoles";
23+
import { userBanningApi } from "@/api/services/userBanning";
24+
import { selectActiveOrganizationId } from "@/store/slices/rolesSlice";
25+
26+
interface TargetUserOrganization {
27+
organization_id: string;
28+
organization_name: string;
29+
}
30+
31+
interface Props {
32+
user: UserProfile;
33+
onSuccess?: () => void;
34+
}
35+
36+
const UnbanUser = ({ user, onSuccess }: Props) => {
37+
const dispatch = useAppDispatch();
38+
const loading = useAppSelector(selectUserBanningLoading);
39+
const activeOrgId = useAppSelector(selectActiveOrganizationId);
40+
const { lang } = useLanguage();
41+
const { allUserRoles, refreshAllUserRoles, hasAnyRole, hasRole } = useRoles();
42+
43+
const isSuper = hasAnyRole(["super_admin", "superVera"]);
44+
const isTenantAdmin = hasRole("tenant_admin");
45+
46+
const canUnbanFromApp = isSuper;
47+
const canUnbanFromOrg = isSuper || isTenantAdmin;
48+
const canUnbanFromRole = isSuper || isTenantAdmin;
49+
50+
const [banType, setBanType] = useState<BanType>("role");
51+
const [notes, setNotes] = useState("");
52+
const [organizationId, setOrganizationId] = useState("");
53+
const [roleId, setRoleId] = useState("");
54+
const [activeBans, setActiveBans] = useState<SimpleBanHistoryItem[]>([]);
55+
const [bansLoading, setBansLoading] = useState(false);
56+
57+
const getOrganizationsWithActiveBans = (): TargetUserOrganization[] => {
58+
const orgMap = new Map<string, TargetUserOrganization>();
59+
activeBans.forEach((ban) => {
60+
const matchesBanType =
61+
(banType === "organization" && ban.ban_type === "banForOrg") ||
62+
(banType === "role" && ban.ban_type === "banForRole") ||
63+
(banType === "application" && ban.ban_type === "banForApp");
64+
65+
if (
66+
!ban.unbanned_at &&
67+
ban.organization_id &&
68+
ban.action === "banned" &&
69+
matchesBanType
70+
) {
71+
const userRole = allUserRoles.find(
72+
(role) => role.organization_id === ban.organization_id,
73+
);
74+
if (userRole && !orgMap.has(ban.organization_id)) {
75+
const org = {
76+
organization_id: ban.organization_id,
77+
organization_name:
78+
userRole.organization_name ?? "Unknown Organization",
79+
};
80+
orgMap.set(ban.organization_id, org);
81+
}
82+
}
83+
});
84+
return Array.from(orgMap.values());
85+
};
86+
87+
const getRolesWithActiveBansForOrg = (orgId: string) => {
88+
const activeRoleBans = activeBans.filter(
89+
(ban) =>
90+
!ban.unbanned_at &&
91+
ban.action === "banned" &&
92+
ban.ban_type === "banForRole" &&
93+
ban.organization_id === orgId,
94+
);
95+
96+
const rolesWithBans = new Map();
97+
activeRoleBans.forEach((ban) => {
98+
if (ban.role_assignment_id) {
99+
const userRole = allUserRoles.find(
100+
(role) =>
101+
role.organization_id === orgId &&
102+
role.user_id === user.id &&
103+
(role.id === ban.role_assignment_id || role.role_id),
104+
);
105+
if (userRole && userRole.role_id) {
106+
const roleInfo = {
107+
role_id: userRole.role_id,
108+
role_name: userRole.role_name,
109+
};
110+
rolesWithBans.set(userRole.role_id, roleInfo);
111+
}
112+
}
113+
});
114+
115+
return Array.from(rolesWithBans.values());
116+
};
117+
118+
const getActiveBanTypes = useCallback((): BanType[] => {
119+
const activeBanTypes = new Set<BanType>();
120+
121+
activeBans.forEach((ban) => {
122+
if (!ban.unbanned_at && ban.action === "banned") {
123+
if (ban.ban_type === "banForApp") activeBanTypes.add("application");
124+
else if (ban.ban_type === "banForOrg")
125+
activeBanTypes.add("organization");
126+
else if (ban.ban_type === "banForRole") activeBanTypes.add("role");
127+
}
128+
});
129+
130+
return Array.from(activeBanTypes);
131+
}, [activeBans]);
132+
133+
const hasActiveApplicationBan = () =>
134+
activeBans.some(
135+
(ban) =>
136+
!ban.unbanned_at &&
137+
ban.action === "banned" &&
138+
ban.ban_type === "banForApp",
139+
);
140+
const hasActiveOrganizationBans = () =>
141+
activeBans.some(
142+
(ban) =>
143+
!ban.unbanned_at &&
144+
ban.action === "banned" &&
145+
ban.ban_type === "banForOrg" &&
146+
ban.organization_id,
147+
);
148+
const hasActiveRoleBans = () =>
149+
activeBans.some(
150+
(ban) =>
151+
!ban.unbanned_at &&
152+
ban.action === "banned" &&
153+
ban.ban_type === "banForRole" &&
154+
ban.organization_id,
155+
);
156+
157+
useEffect(() => {
158+
const loadActiveBans = async () => {
159+
setBansLoading(true);
160+
try {
161+
const bans = await userBanningApi.getUserBanHistory(user.id);
162+
setActiveBans(bans);
163+
} catch (error) {
164+
console.error("Failed to load user ban history:", error);
165+
toast.error(t.unbanUser.messages.failedLoadBanHistory[lang]);
166+
} finally {
167+
setBansLoading(false);
168+
}
169+
};
170+
171+
void loadActiveBans();
172+
if (!allUserRoles || allUserRoles.length === 0) {
173+
void refreshAllUserRoles();
174+
}
175+
}, [user.id, allUserRoles, refreshAllUserRoles, lang]);
176+
177+
useEffect(() => {
178+
if (banType === "role" && organizationId) setRoleId("");
179+
}, [organizationId, banType]);
180+
181+
useEffect(() => {
182+
setOrganizationId("");
183+
setRoleId("");
184+
}, [banType]);
185+
186+
useEffect(() => {
187+
if (activeBans.length > 0) {
188+
const activeBanTypes = getActiveBanTypes();
189+
if (activeBanTypes.length > 0 && !activeBanTypes.includes(banType)) {
190+
setBanType(activeBanTypes[0]);
191+
setOrganizationId("");
192+
setRoleId("");
193+
}
194+
}
195+
}, [activeBans, banType, getActiveBanTypes]);
196+
197+
const handleSubmit = async () => {
198+
if (!user.id) return;
199+
200+
if (banType === "application" && !canUnbanFromApp) {
201+
toast.error(t.unbanUser.messages.noPermissionUnbanApp[lang]);
202+
return;
203+
}
204+
if (banType === "organization" && !canUnbanFromOrg) {
205+
toast.error(t.unbanUser.messages.noPermissionUnbanOrg[lang]);
206+
return;
207+
}
208+
if (banType === "role" && !canUnbanFromRole) {
209+
toast.error(t.unbanUser.messages.noPermissionUnbanRole[lang]);
210+
return;
211+
}
212+
213+
if (banType === "role" && (!organizationId || !roleId)) {
214+
toast.error(t.unbanUser.messages.missingFields[lang]);
215+
return;
216+
}
217+
218+
if (banType === "organization" && !organizationId) {
219+
toast.error(t.unbanUser.messages.missingFields[lang]);
220+
return;
221+
}
222+
223+
if (
224+
(banType === "organization" || banType === "role") &&
225+
isTenantAdmin &&
226+
!isSuper
227+
) {
228+
if (activeOrgId && organizationId !== activeOrgId) {
229+
toast.error(t.unbanUser.messages.onlyUnbanActiveOrg[lang]);
230+
return;
231+
}
232+
}
233+
234+
try {
235+
const result = await dispatch(
236+
unbanUser({
237+
userId: user.id,
238+
banType,
239+
organizationId: organizationId || undefined,
240+
roleId: roleId || undefined,
241+
notes: notes.trim() || undefined,
242+
}),
243+
).unwrap();
244+
245+
if (result.success) {
246+
toast.success(t.unbanUser.toast.unbanSuccess[lang]);
247+
if (onSuccess) onSuccess();
248+
} else {
249+
toast.error(result.message || t.unbanUser.toast.unbanError[lang]);
250+
}
251+
} catch {
252+
toast.error(t.unbanUser.toast.unbanError[lang]);
253+
}
254+
};
255+
256+
return (
257+
<div className="space-y-3">
258+
{bansLoading ? (
259+
<div className="text-center py-4 text-muted-foreground">
260+
{t.unbanUser.messages.loadingBanInfo[lang]}
261+
</div>
262+
) : activeBans.some(
263+
(ban) => !ban.unbanned_at && ban.action === "banned",
264+
) ? (
265+
<>
266+
<div className="space-y-2">
267+
<Label>{t.unbanUser.unban.fields.banTypeToRemove[lang]}</Label>
268+
<Select
269+
value={banType}
270+
onValueChange={(value: BanType) => setBanType(value)}
271+
>
272+
<SelectTrigger>
273+
<SelectValue />
274+
</SelectTrigger>
275+
<SelectContent>
276+
{hasActiveApplicationBan() && canUnbanFromApp && (
277+
<SelectItem value="application">
278+
{t.unbanUser.unban.fields.selectTypes.application[lang]}
279+
</SelectItem>
280+
)}
281+
{hasActiveOrganizationBans() && canUnbanFromOrg && (
282+
<SelectItem value="organization">
283+
{t.unbanUser.unban.fields.selectTypes.organization[lang]}
284+
</SelectItem>
285+
)}
286+
{hasActiveRoleBans() && canUnbanFromRole && (
287+
<SelectItem value="role">
288+
{t.unbanUser.unban.fields.selectTypes.role[lang]}
289+
</SelectItem>
290+
)}
291+
</SelectContent>
292+
</Select>
293+
</div>
294+
295+
{(banType === "organization" || banType === "role") && (
296+
<div className="space-y-2">
297+
<Label>{t.unbanUser.fields.organization.label[lang]}</Label>
298+
<Select value={organizationId} onValueChange={setOrganizationId}>
299+
<SelectTrigger>
300+
<SelectValue
301+
placeholder={
302+
t.unbanUser.unban.fields.organizationPlaceholder[lang]
303+
}
304+
/>
305+
</SelectTrigger>
306+
<SelectContent>
307+
{getOrganizationsWithActiveBans().map((org) => (
308+
<SelectItem
309+
key={org.organization_id}
310+
value={org.organization_id}
311+
>
312+
{org.organization_name}
313+
</SelectItem>
314+
))}
315+
</SelectContent>
316+
</Select>
317+
</div>
318+
)}
319+
320+
{banType === "role" && organizationId && (
321+
<div className="space-y-2">
322+
<Label>{t.unbanUser.fields.role.label[lang]}</Label>
323+
<Select value={roleId} onValueChange={setRoleId}>
324+
<SelectTrigger>
325+
<SelectValue
326+
placeholder={t.unbanUser.unban.fields.rolePlaceholder[lang]}
327+
/>
328+
</SelectTrigger>
329+
<SelectContent>
330+
{getRolesWithActiveBansForOrg(organizationId).map((role) => (
331+
<SelectItem key={role.role_id} value={role.role_id}>
332+
{role.role_name}
333+
</SelectItem>
334+
))}
335+
</SelectContent>
336+
</Select>
337+
</div>
338+
)}
339+
340+
<div className="space-y-2">
341+
<Label>{t.unbanUser.fields.notes.label[lang]}</Label>
342+
<Textarea
343+
value={notes}
344+
onChange={(e) => setNotes(e.target.value)}
345+
rows={2}
346+
placeholder={t.unbanUser.unban.fields.reasonPlaceholder[lang]}
347+
/>
348+
</div>
349+
350+
<div className="flex gap-2">
351+
<Button
352+
onClick={handleSubmit}
353+
disabled={
354+
loading ||
355+
bansLoading ||
356+
!activeBans.some(
357+
(ban) => !ban.unbanned_at && ban.action === "banned",
358+
)
359+
}
360+
>
361+
{loading
362+
? t.unbanUser.toast.loading[lang]
363+
: t.unbanUser.actions.unban[lang]}
364+
</Button>
365+
</div>
366+
</>
367+
) : (
368+
<div className="text-center py-4 text-muted-foreground">
369+
{t.unbanUser.messages.noActiveBans[lang]}
370+
</div>
371+
)}
372+
</div>
373+
);
374+
};
375+
376+
export default UnbanUser;

0 commit comments

Comments
 (0)