Refresh dashboard, adopt MUI X, and improve AI follow-ups

This commit is contained in:
cesnimda
2026-03-23 21:23:15 +01:00
parent 7293582376
commit 66d924e880
9 changed files with 684 additions and 251 deletions
+98 -60
View File
@@ -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>
);
}