Files
jobtrackingapp/job-tracker-ui/src/components/UserManagementCard.tsx
T

184 lines
5.9 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { Box, Button, Paper, TextField, Typography } from "@mui/material";
import { api } from "../api";
import { useToast } from "../toast";
import { useDialogActions } from "../dialogs";
import { useI18n } from "../i18n/I18nProvider";
type UserDto = {
id: string;
email?: string | null;
userName?: string | null;
emailConfirmed: boolean;
roles: string[];
};
export default function UserManagementCard() {
const { toast } = useToast();
const { confirmAction } = useDialogActions();
const { t } = useI18n();
const [supported, setSupported] = useState<boolean | null>(null);
const [users, setUsers] = useState<UserDto[]>([]);
const [loading, setLoading] = useState(false);
const [newEmail, setNewEmail] = useState("");
const [newPassword, setNewPassword] = useState("");
const [newIsAdmin, setNewIsAdmin] = useState(false);
async function load() {
setLoading(true);
try {
const res = await api.get<UserDto[]>("/users");
setUsers(res.data ?? []);
setSupported(true);
} catch (e: any) {
const status = e?.response?.status;
if (status === 401 || status === 403) {
setSupported(false);
} else {
toast(t("adminUsersLoadFailed"), "error");
setSupported(false);
}
} finally {
setLoading(false);
}
}
useEffect(() => {
void load();
const onAuthChanged = () => { void load(); };
window.addEventListener("auth-changed", onAuthChanged);
return () => window.removeEventListener("auth-changed", onAuthChanged);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (supported === false) return null;
if (supported === null) return null;
return (
<Paper sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
{t("adminUsersTitle")}
</Typography>
<Typography sx={{ color: "text.secondary", mb: 2 }}>
{t("adminUsersSubtitle")}
</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.5 }}>
<TextField
label={t("profileEmail")}
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
fullWidth
/>
<TextField
label={t("adminUsersPassword")}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
type="password"
fullWidth
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, mt: 1.5 }}>
<Button
variant={newIsAdmin ? "contained" : "outlined"}
onClick={() => setNewIsAdmin((v) => !v)}
>
{newIsAdmin ? t("adminUsersAdminYes") : t("adminUsersAdminNo")}
</Button>
<Button
variant="contained"
disabled={loading}
onClick={async () => {
try {
await api.post("/users", {
email: newEmail,
password: newPassword,
roles: newIsAdmin ? ["Admin"] : [],
});
setNewEmail("");
setNewPassword("");
setNewIsAdmin(false);
toast(t("adminUsersCreated"), "success");
await load();
} catch (e: any) {
const msg = e?.response?.data || e?.message || t("adminUsersCreateFailed");
toast(String(msg), "error");
}
}}
>
{t("adminUsersCreateUser")}
</Button>
</Box>
<Box sx={{ mt: 2, display: "flex", flexDirection: "column", gap: 1 }}>
{users.length === 0 ? (
<Typography sx={{ color: "text.secondary" }}>{t("adminUsersNoUsers")}</Typography>
) : (
users.map((u) => (
<Box
key={u.id}
sx={{
border: "1px solid rgba(15,23,42,0.08)",
borderRadius: 2,
p: 1.25,
background: "rgba(255,255,255,0.6)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 2,
flexWrap: "wrap",
}}
>
<Box>
<Typography sx={{ fontWeight: 900, lineHeight: 1.2 }}>
{u.userName || u.email || u.id}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{t("adminUsersRolesLabel")}: {u.roles?.length ? u.roles.join(", ") : "—"}
</Typography>
</Box>
<Button
variant="outlined"
color="error"
onClick={async () => {
if (!(await confirmAction(t("adminUsersDeleteConfirmBody"), { title: t("adminUsersDeleteConfirmTitle"), confirmLabel: t("adminUsersDelete"), destructive: true }))) return;
try {
await api.delete(`/users/${u.id}`);
toast(t("adminUsersDeleted"), "info");
await load();
} catch {
toast(t("adminUsersDeleteFailed"), "error");
}
}}
>
{t("adminUsersDelete")}
</Button>
<Button
variant="outlined"
onClick={async () => {
try {
await api.post(`/users/${u.id}/send-password-reset`);
toast(t("adminUsersResetSent"), "success");
} catch (e: any) {
const msg = e?.response?.data || e?.message || t("adminUsersResetFailed");
toast(String(msg), "error");
}
}}
>
{t("adminUsersSendReset")}
</Button>
</Box>
))
)}
</Box>
</Paper>
);
}