AccountInfo

This commit is contained in:
Tim
2025-06-13 18:42:39 +02:00
parent 730bfac6e8
commit 3adbc9a1d1
7 changed files with 288 additions and 172 deletions

View File

@@ -16,6 +16,7 @@
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-charts": "^8.5.1",
"@mui/x-data-grid": "^8.5.2",
"@tanstack/react-query": "^5.79.2",
"chart.js": "^4.4.9",
"i18next": "^25.2.0",
@@ -1550,6 +1551,65 @@
"robust-predicates": "^3.0.2"
}
},
"node_modules/@mui/x-data-grid": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.5.2.tgz",
"integrity": "sha512-4KzawLZqRKp3KcGKsTDVz7zkEjACllQD5Zb8ds1QKlA6C3/oIoSU7PsemFLj+RL3rT5aORsLMBl97/egQ5tUhA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/x-internals": "8.5.2",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz",
"integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"reselect": "^5.1.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/x-internals": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.1.tgz",

View File

@@ -20,6 +20,7 @@
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-charts": "^8.5.1",
"@mui/x-data-grid": "^8.5.2",
"@tanstack/react-query": "^5.79.2",
"chart.js": "^4.4.9",
"i18next": "^25.2.0",

View File

@@ -99,5 +99,9 @@
"admin": "Admin",
"fsmodel": "Indoor-Farming-Station",
"zip": "Postleitzahl",
"surname": "Nachname"
"surname": "Nachname",
"language": "Sprache",
"password": "Passwort",
"adminize": "Admin ernennen",
"unadminize": "Admin abernennen",
}

View File

@@ -99,5 +99,9 @@
"admin": "Admin",
"fsmodel": "Indoor farming station",
"zip": "Zip code",
"surname": "Surname"
"surname": "Surname",
"language": "Language",
"password": "Password",
"adminize": "Make Admin",
"unadminize": "Revoke Admin"
}

View File

@@ -1,187 +1,224 @@
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import {
Box,
Button,
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
useTheme
} from "@mui/material";
import { useState } from "react";
import {Box, Button, IconButton, Toolbar, useTheme} from "@mui/material";
import AccountType from "../../components/Account";
const mockAccounts: AccountType[] = [
{ id: "1", name: "John Doe", email: "john.doe@example.com", status: "active" },
{ id: "2", name: "Jane Smith", email: "jane.smith@example.com", status: "inactive" },
{ id: "3", name: "Alice Johnson", email: "alice.johnson@example.com", status: "active" },
];
import {useTranslation} from "react-i18next";
import {DataGrid, GridColDef, GridRowId, GridRowSelectionModel} from "@mui/x-data-grid";
import {useState} from "react";
export default function AccountsInfo() {
const theme = useTheme();
const {t} = useTranslation();
const [accounts, setAccounts] = useState<AccountType[]>(mockAccounts);
const [searchTerm, setSearchTerm] = useState<string>("");
const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({});
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value.toLowerCase());
};
const handleDelete = (id: string) => {
setAccounts((prev) => prev.filter((account) => account.id !== id));
};
const handleToggleStatus = (id: string) => {
setAccounts((prev) =>
prev.map((account) =>
account.id === id
? {
...account,
status: account.status === "active" ? "inactive" : "active"
function handleCustomerEdit(account: AccountType) {
//TODO: implement
console.log("CustomerEdit", account);
}
: account
)
);
function handleAccountPasswordEdit(account: AccountType) {
//TODO: implement
console.log("PasswordEdit", account);
}
const _rows: AccountType[] = [
{
id: 1,
customer: {id: 0, name: 'Big', surname: 'Smoke', address: "Grove Str", country: "USA", zip: "66424"},
langI18n: "en",
admin: false,
email: "smoke@trainwatchers.com",
password: "$1$s11alt$h111ash"
},
{
id: 2,
customer: {id: 0, name: 'C', surname: 'J', address: "Grove Str", country: "USA", zip: "66424"},
langI18n: "en",
admin: true,
email: "cj@grv.gov",
password: "$1$salt$hash"
},
{
id: 3,
customer: {
id: 0,
name: 'Flo',
surname: 'Rian',
address: "Weihbachstreet 555",
country: "Germany",
zip: "66424"
},
langI18n: "de",
admin: false,
email: "flo@ryan",
password: "$1$salt$hash"
},
{
id: 41,
customer: {
id: 0,
name: 'Tim',
surname: 'Wand',
address: "Wandstraße 12",
country: "Deutschland",
zip: "66420"
},
langI18n: "de",
admin: true,
email: "management@shitposting.eu",
password: "$5$sa23123lt$has21212h"
},
{
id: 15,
customer: {
id: 0,
name: 'Lia',
surname: 'Shaprius',
address: "Malstatter Str 18",
country: "Saarbrooklyn",
zip: "66117"
},
langI18n: "de",
admin: false,
email: "lia.prius@outlook.de",
password: "$1$salt$hash"
},
{
id: 17,
customer: {
id: 0,
name: 'Mathu-san',
surname: 'Saravanpavan',
address: "Solarbro-Alee",
country: "Deutschland",
zip: "66000"
},
langI18n: "de",
admin: false,
email: "mathusan@bro.de",
password: "$12$salt12$hash123"
},
{
id: 81,
customer: {id: 0, name: 'C', surname: 'J', address: "Grove Str", country: "USA", zip: "66424"},
langI18n: "en",
admin: false,
email: "flo@ryan",
password: "$1$salt$hash"
},
{
id: 19,
customer: {id: 0, name: 'C', surname: 'J', address: "Grove Str", country: "USA", zip: "66424"},
langI18n: "en",
admin: false,
email: "flo@ryan",
password: "$1$salt$hash"
},
]
/*
for testing purposes
for (let i = 0; i < 100; i++) {
_rows.push(_rows[5]);
}
* */
//TODO: get per REST
const [rows, setRows] = useState<AccountType[]>(_rows);
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
const handleSelectionChange = (newSelection: GridRowSelectionModel) => {
setSelectedRows(newSelection.ids);
};
const handleEdit = (id: string, field: keyof AccountType, value: string) => {
setAccounts((prev) =>
prev.map((account) =>
account.id === id ? { ...account, [field]: value } : account
)
);
const handleDeleteSelected = async () => {
selectedRows.forEach((row) => {
//TODO: send delete command, or send deleteall
console.log(row);
})
setRows(rows.filter((row) => !selectedRows.has(row.id)));
};
const toggleEditMode = (id: string) => {
setEditMode((prev) => ({
...prev,
[id]: !prev[id]
}));
};
const filteredAccounts = accounts.filter(
(account) =>
account.name.toLowerCase().includes(searchTerm) ||
account.email.toLowerCase().includes(searchTerm)
);
const columns: GridColDef<(typeof rows)[number]>[] = [
{field: 'id', headerName: 'ID', width: 60},
{
field: 'admin',
headerName: t('admin'),
type: "boolean",
width: 90,
editable: true
},
{
field: 'email',
headerName: t('email'),
width: 150,
editable: true,
},
{
field: 'password',
headerName: t('password'),
width: 90,
editable: true,
sortable: false,
disableReorder: true,
disableExport: true,
renderCell: params => <IconButton onClick={() => handleAccountPasswordEdit(params.row)}> <EditIcon/>
</IconButton>,
},
{
field: 'langI18n',
headerName: t('language'),
width: 150,
editable: true,
},
{ //edit billing information button
field: "customer",
headerName: t('address'),
width: 90,
sortable: false,
disableReorder: true,
disableColumnMenu: true,
renderCell: params => <IconButton onClick={() => handleCustomerEdit(params.row)}> <EditIcon/> </IconButton>,
}
];
return (
<Box sx={{ color: theme.palette.text.primary }}>
<Typography variant="h4" gutterBottom align="center">
Accounts Management
</Typography>
<TextField
label="Search Accounts"
variant="outlined"
fullWidth
<Box
className="page-table"
sx={{
mb: 2,
backgroundColor: theme.palette.background.paper,
'& input': {
color: theme.palette.text.primary
},
'& label': {
color: theme.palette.text.secondary
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: theme.palette.divider
},
'&:hover fieldset': {
borderColor: theme.palette.text.primary
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main
}
}
}}
onChange={handleSearch}
/>
<TableContainer
component={Paper}
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary
}}
>
<Table>
<TableHead>
<TableRow>
{["ID", "Name", "Email", "Status", "Actions"].map((col) => (
<TableCell
key={col}
sx={{ color: theme.palette.text.primary, fontWeight: "bold" }}
>
{col}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{filteredAccounts.map((account) => (
<TableRow key={account.id}>
<TableCell>{account.id}</TableCell>
<TableCell>
{editMode[account.id] ? (
<TextField
value={account.name}
onChange={(e) => handleEdit(account.id, "name", e.target.value)}
variant="standard"
InputProps={{
sx: { color: theme.palette.text.primary }
}}
/>
) : (
account.name
)}
</TableCell>
<TableCell>
{editMode[account.id] ? (
<TextField
value={account.email}
onChange={(e) => handleEdit(account.id, "email", e.target.value)}
variant="standard"
InputProps={{
sx: { color: theme.palette.text.primary }
}}
/>
) : (
account.email
)}
</TableCell>
<TableCell>{account.status}</TableCell>
<TableCell align="center">
<IconButton color="error" onClick={() => handleDelete(account.id)}>
<DeleteIcon />
</IconButton>
<DataGrid
rows={rows}
columns={columns}
initialState={{}}
checkboxSelection
disableRowSelectionOnClick
onRowSelectionModelChange={handleSelectionChange}
slots={{ toolbar: () => (
<Toolbar>
<Button
variant="contained"
color={account.status === "active" ? "warning" : "success"}
onClick={() => handleToggleStatus(account.id)}
sx={{ mx: 1 }}
color="error"
startIcon={<DeleteIcon/>}
onClick={handleDeleteSelected}
disabled={selectedRows.size === 0}
sx={{
marginRight: 1
}}
>
{account.status === "active" ? "Set Inactive" : "Set Active"}
{t('deleteAccount')}
</Button>
<IconButton color="primary" onClick={() => toggleEditMode(account.id)}>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Toolbar>
)}}
showToolbar
processRowUpdate={(updatedRow, originalRow) => {
setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row));
//TODO: make REST callback
return updatedRow;
}}
/>
</Box>
);
}

View File

@@ -95,7 +95,7 @@ export default function AdminPanel() {
</Box>
{/* Content */}
<Box className="page-background" pt={4}>{renderContent()}</Box>
<Box className="page-background">{renderContent()}</Box>
</div>
</Box>
);

View File

@@ -58,11 +58,13 @@
display: flex;
flex-direction: column;
overflow: auto;
padding: 20px 0;
box-sizing: border-box;
width: 100%;
color: var(--text-color);
}
.toppad {
padding: 20px 0;
}
.page-background-center {
align-items: center;
@@ -73,6 +75,14 @@
justify-content: flex-start !important;
}
.page-table {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: var(--page-height);
}
.impressum-container {
display: flex;
flex-direction: column;