Refresh dashboard, adopt MUI X, and improve AI follow-ups
This commit is contained in:
@@ -1,20 +1,16 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
FormControlLabel,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
|
||||
import { api } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
@@ -58,7 +54,16 @@ export default function AdminUsersPage() {
|
||||
|
||||
const canCreate = useMemo(() => newEmail.trim().length > 3 && newPassword.length >= 6, [newEmail, newPassword]);
|
||||
|
||||
const setAdminRole = async (u: UserDto, isAdmin: boolean) => {
|
||||
const rows = useMemo(() => users.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email || "",
|
||||
userName: u.userName || "",
|
||||
roles: u.roles || [],
|
||||
emailConfirmed: u.emailConfirmed,
|
||||
raw: u,
|
||||
})), [users]);
|
||||
|
||||
const setAdminRole = useCallback(async (u: UserDto, isAdmin: boolean) => {
|
||||
try {
|
||||
await api.put(`/users/${u.id}/roles`, { roles: isAdmin ? ["Admin"] : [] });
|
||||
toast(t("adminUsersRolesUpdated"), "success");
|
||||
@@ -67,9 +72,9 @@ export default function AdminUsersPage() {
|
||||
const msg = e?.response?.data || e?.message || t("adminUsersRolesUpdateFailed");
|
||||
toast(String(msg), "error");
|
||||
}
|
||||
};
|
||||
}, [t, toast]);
|
||||
|
||||
const sendReset = async (u: UserDto) => {
|
||||
const sendReset = useCallback(async (u: UserDto) => {
|
||||
try {
|
||||
await api.post(`/users/${u.id}/send-password-reset`);
|
||||
toast(t("adminUsersResetSent"), "success");
|
||||
@@ -77,9 +82,9 @@ export default function AdminUsersPage() {
|
||||
const msg = e?.response?.data || e?.message || t("adminUsersResetFailed");
|
||||
toast(String(msg), "error");
|
||||
}
|
||||
};
|
||||
}, [t, toast]);
|
||||
|
||||
const remove = async (u: UserDto) => {
|
||||
const remove = useCallback(async (u: UserDto) => {
|
||||
const name = u.userName || u.email || u.id;
|
||||
if (!(await confirmAction(t("adminUsersDeleteConfirmNamed", { name }), { title: t("adminUsersDeleteConfirmTitle"), confirmLabel: t("adminUsersDelete"), destructive: true }))) return;
|
||||
try {
|
||||
@@ -89,7 +94,60 @@ export default function AdminUsersPage() {
|
||||
} catch {
|
||||
toast(t("adminUsersDeleteFailed"), "error");
|
||||
}
|
||||
};
|
||||
}, [confirmAction, t, toast]);
|
||||
|
||||
const columns = useMemo<GridColDef[]>(() => [
|
||||
{ field: "email", headerName: t("profileEmail"), flex: 1.2, minWidth: 220 },
|
||||
{ field: "userName", headerName: t("profileUsername"), flex: 1, minWidth: 180 },
|
||||
{
|
||||
field: "roles",
|
||||
headerName: t("adminUsersRolesLabel"),
|
||||
flex: 1,
|
||||
minWidth: 180,
|
||||
sortable: false,
|
||||
renderCell: (params) => {
|
||||
const roles = params.row.roles as string[];
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", py: 0.5 }}>
|
||||
{roles.length ? roles.map((role) => <Chip key={role} size="small" label={role} variant="outlined" />) : <Typography variant="body2" sx={{ color: "text.secondary" }}>—</Typography>}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "emailConfirmed",
|
||||
headerName: t("adminUsersConfirmed"),
|
||||
width: 130,
|
||||
renderCell: (params) => (
|
||||
<Chip size="small" label={params.value ? t("yes") : t("noWord")} color={params.value ? "success" : "default"} variant={params.value ? "filled" : "outlined"} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: t("adminUsersActions"),
|
||||
minWidth: 300,
|
||||
flex: 1.4,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
renderCell: (params) => {
|
||||
const user = params.row.raw as UserDto;
|
||||
const isAdmin = (user.roles || []).includes("Admin");
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", py: 0.5 }}>
|
||||
<Button size="small" variant={isAdmin ? "contained" : "outlined"} onClick={() => void setAdminRole(user, !isAdmin)}>
|
||||
{t("adminUsersAdmin")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => void sendReset(user)}>
|
||||
{t("adminUsersSendReset")}
|
||||
</Button>
|
||||
<Button size="small" color="error" variant="outlined" onClick={() => void remove(user)}>
|
||||
{t("adminUsersDelete")}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
], [remove, sendReset, setAdminRole, t]);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2 }}>
|
||||
@@ -128,53 +186,33 @@ export default function AdminUsersPage() {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<TableContainer sx={{ borderRadius: 2, border: "1px solid", borderColor: "divider" }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("profileEmail")}</TableCell>
|
||||
<TableCell>{t("profileUsername")}</TableCell>
|
||||
<TableCell>{t("adminUsersRolesLabel")}</TableCell>
|
||||
<TableCell>{t("adminUsersConfirmed")}</TableCell>
|
||||
<TableCell align="right">{t("adminUsersActions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((u) => {
|
||||
const isAdmin = (u.roles || []).includes("Admin");
|
||||
return (
|
||||
<TableRow key={u.id} hover>
|
||||
<TableCell sx={{ fontWeight: 850 }}>{u.email || ""}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{u.userName || ""}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{u.roles?.length ? u.roles.join(", ") : "-"}</TableCell>
|
||||
<TableCell sx={{ color: "text.secondary" }}>{u.emailConfirmed ? t("yes") : t("noWord")}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button size="small" variant={isAdmin ? "contained" : "outlined"} onClick={() => void setAdminRole(u, !isAdmin)}>
|
||||
{t("adminUsersAdmin")}
|
||||
</Button>
|
||||
<Button size="small" variant="outlined" onClick={() => void sendReset(u)}>
|
||||
{t("adminUsersSendReset")}
|
||||
</Button>
|
||||
<Button size="small" color="error" variant="outlined" onClick={() => void remove(u)}>
|
||||
{t("adminUsersDelete")}
|
||||
</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
{!loading && users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5}>
|
||||
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("adminUsersNoUsers")}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Paper sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider", overflow: "hidden" }}>
|
||||
<DataGrid
|
||||
autoHeight
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
disableRowSelectionOnClick
|
||||
loading={loading}
|
||||
pageSizeOptions={[5, 10, 25]}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 10, page: 0 } },
|
||||
sorting: { sortModel: [{ field: "email", sort: "asc" }] },
|
||||
}}
|
||||
sx={{
|
||||
border: 0,
|
||||
'& .MuiDataGrid-columnHeaders': {
|
||||
backgroundColor: 'action.hover',
|
||||
fontWeight: 800,
|
||||
},
|
||||
'& .MuiDataGrid-cell': {
|
||||
alignItems: 'center',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{!loading && rows.length === 0 ? (
|
||||
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>{t("adminUsersNoUsers")}</Typography>
|
||||
) : null}
|
||||
</Paper>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user