init /user route only for admin user

This commit is contained in:
maxim 2025-03-28 03:54:15 +03:00
parent 3eb56a27ae
commit b4afcdfa3b
11 changed files with 338 additions and 6 deletions

View File

@ -24,9 +24,11 @@ import {SightList, SightCreate, SightEdit, SightShow} from './pages/sight'
import {StationList, StationCreate, StationEdit, StationShow} from './pages/station' import {StationList, StationCreate, StationEdit, StationShow} from './pages/station'
import {VehicleList, VehicleCreate, VehicleEdit, VehicleShow} from './pages/vehicle' import {VehicleList, VehicleCreate, VehicleEdit, VehicleShow} from './pages/vehicle'
import {RouteList, RouteCreate, RouteEdit, RouteShow} from './pages/route' import {RouteList, RouteCreate, RouteEdit, RouteShow} from './pages/route'
import {UserList, UserCreate, UserEdit, UserShow} from './pages/user'
import {CountryIcon, CityIcon, CarrierIcon, MediaIcon, ArticleIcon, SightIcon, StationIcon, VehicleIcon, RouteIcon} from './components/ui/Icons' import {CountryIcon, CityIcon, CarrierIcon, MediaIcon, ArticleIcon, SightIcon, StationIcon, VehicleIcon, RouteIcon, UsersIcon} from './components/ui/Icons'
import SidebarTitle from './components/ui/SidebarTitle' import SidebarTitle from './components/ui/SidebarTitle'
import {AdminOnly} from './components/AdminOnly'
function App() { function App() {
return ( return (
@ -134,7 +136,6 @@ function App() {
create: '/vehicle/create', create: '/vehicle/create',
edit: '/vehicle/edit/:id', edit: '/vehicle/edit/:id',
show: '/vehicle/show/:id', show: '/vehicle/show/:id',
// добавить SHOW для vehicle->routes (https://wn.krbl.ru/vehicle/routes?id=1)
meta: { meta: {
canDelete: true, canDelete: true,
label: 'Транспорт', label: 'Транспорт',
@ -147,14 +148,24 @@ function App() {
create: '/route/create', create: '/route/create',
edit: '/route/edit/:id', edit: '/route/edit/:id',
show: '/route/show/:id', show: '/route/show/:id',
// добавить SHOW для route->station (https://wn.krbl.ru/route/station)
// добавить SHOW для route->vehicle (https://wn.krbl.ru/route/vehicle)
meta: { meta: {
canDelete: true, canDelete: true,
label: 'Маршруты', label: 'Маршруты',
icon: <RouteIcon />, icon: <RouteIcon />,
}, },
}, },
{
name: 'user',
list: '/user',
create: '/user/create',
edit: '/user/edit/:id',
show: '/user/show/:id',
meta: {
canDelete: true,
label: 'Пользователи',
icon: <UsersIcon />,
},
},
]} ]}
options={{ options={{
syncWithLocation: true, syncWithLocation: true,
@ -238,6 +249,41 @@ function App() {
<Route path="show/:id" element={<RouteShow />} /> <Route path="show/:id" element={<RouteShow />} />
</Route> </Route>
<Route path="/user">
<Route
index
element={
<AdminOnly>
<UserList />
</AdminOnly>
}
/>
<Route
path="create"
element={
<AdminOnly>
<UserCreate />
</AdminOnly>
}
/>
<Route
path="edit/:id"
element={
<AdminOnly>
<UserEdit />
</AdminOnly>
}
/>
<Route
path="show/:id"
element={
<AdminOnly>
<UserShow />
</AdminOnly>
}
/>
</Route>
<Route path="*" element={<ErrorComponent />} /> <Route path="*" element={<ErrorComponent />} />
</Route> </Route>
<Route <Route

View File

@ -129,8 +129,14 @@ export const authProvider: AuthProvider = {
try { try {
const decoded = jwtDecode<JWTPayload>(token) const decoded = jwtDecode<JWTPayload>(token)
if (decoded.is_admin) {
document.body.classList.add('is-admin')
} else {
document.body.classList.remove('is-admin')
}
return decoded.is_admin ? ['admin'] : ['user'] return decoded.is_admin ? ['admin'] : ['user']
} catch { } catch {
document.body.classList.remove('is-admin')
return ['user'] return ['user']
} }
}, },

View File

@ -8,5 +8,18 @@ import CastleIcon from '@mui/icons-material/Castle'
import HailIcon from '@mui/icons-material/Hail' import HailIcon from '@mui/icons-material/Hail'
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus' import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'
import ForkLeftIcon from '@mui/icons-material/ForkLeft' import ForkLeftIcon from '@mui/icons-material/ForkLeft'
import PeopleAltIcon from '@mui/icons-material/PeopleAlt'
export {BedtimeIcon as ProjectIcon, PublicIcon as CountryIcon, LocationCityIcon as CityIcon, TramIcon as CarrierIcon, PermMediaIcon as MediaIcon, FeedIcon as ArticleIcon, CastleIcon as SightIcon, HailIcon as StationIcon, DirectionsBusIcon as VehicleIcon, ForkLeftIcon as RouteIcon} export {
BedtimeIcon as ProjectIcon,
PublicIcon as CountryIcon,
LocationCityIcon as CityIcon,
TramIcon as CarrierIcon,
PermMediaIcon as MediaIcon,
FeedIcon as ArticleIcon,
CastleIcon as SightIcon,
HailIcon as StationIcon,
DirectionsBusIcon as VehicleIcon,
ForkLeftIcon as RouteIcon,
PeopleAltIcon as UsersIcon, // users icon
}

View File

@ -112,5 +112,12 @@
"edit": "Редактировать маршрут", "edit": "Редактировать маршрут",
"show": "Показать маршрут" "show": "Показать маршрут"
} }
},
"user": {
"titles": {
"create": "Создать пользователя",
"edit": "Редактировать пользователя",
"show": "Показать пользователя"
}
} }
} }

View File

@ -70,13 +70,13 @@ export const RouteCreate = () => {
label={'Номер маршрута *'} label={'Номер маршрута *'}
name="route_number" name="route_number"
/> />
<Controller <Controller
name="route_direction" // boolean name="route_direction" // boolean
control={control} control={control}
defaultValue={false} defaultValue={false}
render={({field}: {field: any}) => <FormControlLabel label="Прямой маршрут? *" control={<Checkbox {...field} checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />} />} render={({field}: {field: any}) => <FormControlLabel label="Прямой маршрут? *" control={<Checkbox {...field} checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />} />}
/> />
<Typography variant="caption" color="textSecondary" sx={{mt: 0, mb: 1}}> <Typography variant="caption" color="textSecondary" sx={{mt: 0, mb: 1}}>
(Прямой / Обратный) (Прямой / Обратный)
</Typography> </Typography>

68
src/pages/user/create.tsx Normal file
View File

@ -0,0 +1,68 @@
import {Box, TextField, FormControlLabel, Checkbox} from '@mui/material'
import {Create} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form'
export const UserCreate = () => {
const {
saveButtonProps,
refineCore: {formLoading},
register,
control,
formState: {errors},
} = useForm({})
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
<TextField
{...register('name', {
required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="text"
label={'Имя *'}
name="name"
/>
<TextField
{...register('email', {
required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.email}
helperText={(errors as any)?.email?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="email"
label={'Электронная почта *'}
name="email"
/>
<TextField
{...register('password', {
required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.password}
helperText={(errors as any)?.password?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="text"
label={'Пароль *'}
name="password"
/>
<Controller
name="is_admin" // boolean
control={control}
defaultValue={false}
render={({field}: {field: any}) => <FormControlLabel label="Администратор *" control={<Checkbox {...field} checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />} />}
/>
</Box>
</Create>
)
}

67
src/pages/user/edit.tsx Normal file
View File

@ -0,0 +1,67 @@
import {Box, TextField, FormControlLabel, Checkbox} from '@mui/material'
import {Edit} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form'
export const UserEdit = () => {
const {
saveButtonProps,
register,
control,
formState: {errors},
} = useForm({})
return (
<Edit saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
<TextField
{...register('name', {
required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="text"
label={'Имя *'}
name="name"
/>
<TextField
{...register('email', {
required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.email}
helperText={(errors as any)?.email?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="email"
label={'Электронная почта *'}
name="email"
/>
<TextField
{...register('password', {
// required: 'Это поле является обязательным',
})}
error={!!(errors as any)?.password}
helperText={(errors as any)?.password?.message}
margin="normal"
fullWidth
InputLabelProps={{shrink: true}}
type="text"
label={'Пароль *'}
name="password"
/>
<Controller
name="is_admin" // boolean
control={control}
defaultValue={false}
render={({field}: {field: any}) => <FormControlLabel label="Администратор *" control={<Checkbox {...field} checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />} />}
/>
</Box>
</Edit>
)
}

4
src/pages/user/index.tsx Normal file
View File

@ -0,0 +1,4 @@
export * from './create'
export * from './edit'
export * from './list'
export * from './show'

77
src/pages/user/list.tsx Normal file
View File

@ -0,0 +1,77 @@
import React from 'react'
import {type GridColDef} from '@mui/x-data-grid'
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
import {CustomDataGrid} from '../../components/CustomDataGrid'
import {localeText} from '../../locales/ru/localeText'
import {Typography} from '@mui/material'
export const UserList = () => {
const {dataGridProps} = useDataGrid({})
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
minWidth: 50,
align: 'left',
headerAlign: 'left',
},
{
field: 'name',
headerName: 'Имя',
type: 'string',
minWidth: 250,
align: 'left',
headerAlign: 'left',
},
{
field: 'email',
headerName: 'Email',
type: 'string',
minWidth: 250,
align: 'left',
headerAlign: 'left',
},
{
field: 'is_admin',
headerName: 'Роль',
type: 'boolean',
align: 'left',
headerAlign: 'left',
minWidth: 150,
flex: 1,
// renderCell: ({value}) => <Chip label={value ? 'Да' : 'Нет'} color={value ? 'primary' : 'default'} size="small" />,
renderCell: ({value}) => <Typography style={{color: value ? '#48989f' : '#7f6b58'}}>{value ? 'администратор' : 'пользователь'}</Typography>,
},
{
field: 'actions',
headerName: 'Действия',
minWidth: 120,
display: 'flex',
align: 'right',
headerAlign: 'center',
sortable: false,
filterable: false,
disableColumnMenu: true,
renderCell: function render({row}) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
</>
)
},
},
],
[],
)
return (
<List>
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} />
</List>
)
}

34
src/pages/user/show.tsx Normal file
View File

@ -0,0 +1,34 @@
import {Stack, Typography} from '@mui/material'
import {useShow} from '@refinedev/core'
import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
export const UserShow = () => {
const {query} = useShow({})
const {data, isLoading} = query
const record = data?.data
const fields = [
{label: 'Имя', data: 'name'},
{label: 'Электронная почта', data: 'email'},
{
label: 'Администратор',
data: 'is_admin',
render: (value: number[][]) => <Typography style={{color: value ? '#48989f' : '#7f6b58'}}>{value ? 'администратор' : 'пользователь'}</Typography>,
},
]
return (
<Show isLoading={isLoading}>
<Stack gap={4}>
{fields.map(({label, data, render}) => (
<Stack key={data} gap={1}>
<Typography variant="body1" fontWeight="bold">
{label}
</Typography>
{render ? render(record?.[data]) : <TextField value={record?.[data]} />}
</Stack>
))}
</Stack>
</Show>
)
}

View File

@ -37,3 +37,13 @@
.MuiBox-root div .css-1enbsbt-MuiButtonBase-root-MuiButton-root { .MuiBox-root div .css-1enbsbt-MuiButtonBase-root-MuiButton-root {
display: none !important; display: none !important;
} }
/* Hide users menu item by default */
a[aria-label='Пользователи'] {
display: none !important;
}
/* Show for admin users - this class will be added to body */
body.is-admin a[aria-label='Пользователи'] {
display: flex !important;
}