Enhance role management by adding pricing fields and updating related logic
- Updated `CreateRoleCommand` and `UpdateRoleCommand` to include optional pricing fields: `PricePerConfigPerQuarter` and `PricePerConfigPerYear`. - Modified `RoleEndpoints` to handle the new pricing parameters during role creation and updates. - Enhanced validation logic in `CreateRoleCommandValidator` and `UpdateRoleCommandValidator` to ensure pricing fields are non-negative when provided. - Updated `RoleDto` and `SelectableRoleDto` to include pricing information, ensuring proper data handling in API responses. - Adjusted frontend components to support new pricing fields in role forms and display total costs based on configurations. - Updated API documentation to reflect changes in role management endpoints and pricing structure.
This commit is contained in:
@@ -24,17 +24,38 @@ export function RoleFormDialog({
|
||||
const [name, setName] = useState(role?.name ?? '')
|
||||
const [maxConfigs, setMaxConfigs] = useState(String(role?.maxConfigs ?? 3))
|
||||
const [maxIpLimit, setMaxIpLimit] = useState(String(role?.maxIpLimit ?? 2))
|
||||
const [pricePerConfigPerQuarter, setPricePerConfigPerQuarter] = useState(
|
||||
role?.pricePerConfigPerQuarter != null ? String(role.pricePerConfigPerQuarter) : '',
|
||||
)
|
||||
const [pricePerConfigPerYear, setPricePerConfigPerYear] = useState(
|
||||
role?.pricePerConfigPerYear != null ? String(role.pricePerConfigPerYear) : '',
|
||||
)
|
||||
const [internalOpen, setInternalOpen] = useState(false)
|
||||
|
||||
const isControlled = open !== undefined
|
||||
const dialogOpen = isControlled ? open : internalOpen
|
||||
const setDialogOpen = isControlled ? onOpenChange! : setInternalOpen
|
||||
|
||||
// Роль admin оплаты не имеет — поля цены для неё не показываются и не отправляются.
|
||||
const isAdmin = role?.name === 'admin'
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
role
|
||||
? updateRole(role.id, Number(maxConfigs), Number(maxIpLimit))
|
||||
: createRole(name.trim(), Number(maxConfigs), Number(maxIpLimit)),
|
||||
? updateRole(
|
||||
role.id,
|
||||
Number(maxConfigs),
|
||||
Number(maxIpLimit),
|
||||
pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter),
|
||||
pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear),
|
||||
)
|
||||
: createRole(
|
||||
name.trim(),
|
||||
Number(maxConfigs),
|
||||
Number(maxIpLimit),
|
||||
pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter),
|
||||
pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear),
|
||||
),
|
||||
onSuccess: async () => {
|
||||
toast.success(role ? t('admin.roles.updated') : t('admin.roles.created'))
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-roles'] })
|
||||
@@ -42,10 +63,17 @@ export function RoleFormDialog({
|
||||
setName('')
|
||||
setMaxConfigs('3')
|
||||
setMaxIpLimit('2')
|
||||
setPricePerConfigPerQuarter('')
|
||||
setPricePerConfigPerYear('')
|
||||
},
|
||||
onError: () => toast.error(t('auth.genericError')),
|
||||
})
|
||||
|
||||
const totalFor = (price: string) => {
|
||||
if (price === '' || Number(maxConfigs) < 0) return null
|
||||
return Number(price) * Number(maxConfigs)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{!isControlled && (
|
||||
@@ -80,6 +108,40 @@ export function RoleFormDialog({
|
||||
<Input id="maxIpLimit" type="number" value={maxIpLimit} onChange={(e) => setMaxIpLimit(e.target.value)} />
|
||||
<p className="text-xs text-muted-foreground">{t('admin.roles.maxIpLimitHint')}</p>
|
||||
</div>
|
||||
{!isAdmin && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="pricePerConfigPerQuarter">{t('admin.roles.pricePerConfigPerQuarter')}</Label>
|
||||
<Input
|
||||
id="pricePerConfigPerQuarter"
|
||||
type="number"
|
||||
min={0}
|
||||
value={pricePerConfigPerQuarter}
|
||||
onChange={(e) => setPricePerConfigPerQuarter(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('admin.roles.pricePerConfigPerQuarterHint')}
|
||||
{totalFor(pricePerConfigPerQuarter) !== null &&
|
||||
` ${t('admin.roles.totalValue', { total: totalFor(pricePerConfigPerQuarter) })}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="pricePerConfigPerYear">{t('admin.roles.pricePerConfigPerYear')}</Label>
|
||||
<Input
|
||||
id="pricePerConfigPerYear"
|
||||
type="number"
|
||||
min={0}
|
||||
value={pricePerConfigPerYear}
|
||||
onChange={(e) => setPricePerConfigPerYear(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('admin.roles.pricePerConfigPerYearHint')}
|
||||
{totalFor(pricePerConfigPerYear) !== null &&
|
||||
` ${t('admin.roles.totalValue', { total: totalFor(pricePerConfigPerYear) })}`}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button type="submit" disabled={mutation.isPending || (!role && !name.trim())}>
|
||||
{role ? t('admin.roles.save') : t('admin.roles.create')}
|
||||
</Button>
|
||||
|
||||
@@ -5,12 +5,30 @@ export function listRoles() {
|
||||
return apiRequest<RoleDto[]>('/admin/roles')
|
||||
}
|
||||
|
||||
export function createRole(name: string, maxConfigs: number, maxIpLimit: number) {
|
||||
return apiRequest<RoleDto>('/admin/roles', { method: 'POST', body: { name, maxConfigs, maxIpLimit } })
|
||||
export function createRole(
|
||||
name: string,
|
||||
maxConfigs: number,
|
||||
maxIpLimit: number,
|
||||
pricePerConfigPerQuarter: number | null,
|
||||
pricePerConfigPerYear: number | null,
|
||||
) {
|
||||
return apiRequest<RoleDto>('/admin/roles', {
|
||||
method: 'POST',
|
||||
body: { name, maxConfigs, maxIpLimit, pricePerConfigPerQuarter, pricePerConfigPerYear },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRole(id: string, maxConfigs: number, maxIpLimit: number) {
|
||||
return apiRequest<RoleDto>(`/admin/roles/${id}`, { method: 'PUT', body: { maxConfigs, maxIpLimit } })
|
||||
export function updateRole(
|
||||
id: string,
|
||||
maxConfigs: number,
|
||||
maxIpLimit: number,
|
||||
pricePerConfigPerQuarter: number | null,
|
||||
pricePerConfigPerYear: number | null,
|
||||
) {
|
||||
return apiRequest<RoleDto>(`/admin/roles/${id}`, {
|
||||
method: 'PUT',
|
||||
body: { maxConfigs, maxIpLimit, pricePerConfigPerQuarter, pricePerConfigPerYear },
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRole(id: string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiRequest, apiUpload, getAccessToken } from '@/shared/api/client'
|
||||
import type {
|
||||
PagedList,
|
||||
RoleDto,
|
||||
SelectableRoleDto,
|
||||
TicketCommentDto,
|
||||
TicketDetailDto,
|
||||
TicketStatus,
|
||||
@@ -25,7 +25,7 @@ export function getTicket(id: string) {
|
||||
}
|
||||
|
||||
export function listSelectableRoles() {
|
||||
return apiRequest<RoleDto[]>('/support/roles')
|
||||
return apiRequest<SelectableRoleDto[]>('/support/roles')
|
||||
}
|
||||
|
||||
export function createBugReportTicket(message: string, files: File[]) {
|
||||
|
||||
@@ -18,6 +18,9 @@ function AdminRolesPage() {
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['admin-roles'], queryFn: listRoles })
|
||||
|
||||
const totalPrice = (price: number | null, maxConfigs: number) =>
|
||||
price == null || maxConfigs < 0 ? t('admin.roles.noPrice') : `${price * maxConfigs} ₽`
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteRole,
|
||||
onSuccess: async () => {
|
||||
@@ -52,6 +55,8 @@ function AdminRolesPage() {
|
||||
<th className="py-2 font-medium">{t('admin.roles.name')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.roles.maxConfigs')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.roles.maxIpLimit')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.roles.totalPerQuarter')}</th>
|
||||
<th className="py-2 font-medium">{t('admin.roles.totalPerYear')}</th>
|
||||
<th className="py-2" />
|
||||
<th className="py-2" />
|
||||
</tr>
|
||||
@@ -64,6 +69,8 @@ function AdminRolesPage() {
|
||||
</td>
|
||||
<td className="py-2">{role.maxConfigs < 0 ? t('unlimited') : role.maxConfigs}</td>
|
||||
<td className="py-2">{role.maxIpLimit < 0 ? t('unlimited') : role.maxIpLimit}</td>
|
||||
<td className="py-2">{totalPrice(role.pricePerConfigPerQuarter, role.maxConfigs)}</td>
|
||||
<td className="py-2">{totalPrice(role.pricePerConfigPerYear, role.maxConfigs)}</td>
|
||||
<td className="py-2 text-right">
|
||||
<Button size="sm" variant="outline" onClick={() => setEditing(role)}>
|
||||
{t('admin.roles.edit')}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -173,6 +173,16 @@ export type RoleDto = {
|
||||
maxConfigs: number
|
||||
maxIpLimit: number
|
||||
isSystem: boolean
|
||||
pricePerConfigPerQuarter: number | null
|
||||
pricePerConfigPerYear: number | null
|
||||
}
|
||||
|
||||
/** Роль для выбора в заявке на роль (GET /support/roles) — без цены, она видна только админу. */
|
||||
export type SelectableRoleDto = {
|
||||
id: string
|
||||
name: string
|
||||
maxConfigs: number
|
||||
maxIpLimit: number
|
||||
}
|
||||
|
||||
export type ActivationRequestAdminDto = {
|
||||
|
||||
@@ -253,6 +253,14 @@ const resources = {
|
||||
maxConfigsHint: '−1 = без лимита.',
|
||||
maxIpLimit: 'Лимит IP на конфиг',
|
||||
maxIpLimitHint: '−1 = без лимита. Применяется только к новым конфигам.',
|
||||
pricePerConfigPerQuarter: 'Цена за конфиг / 3 мес',
|
||||
pricePerConfigPerQuarterHint: 'Справочно, видно только админу. Минимальный период оплаты.',
|
||||
pricePerConfigPerYear: 'Цена за конфиг / год',
|
||||
pricePerConfigPerYearHint: 'Справочно, видно только админу. Задаётся отдельно от квартальной цены.',
|
||||
totalPerQuarter: 'Итого / 3 мес',
|
||||
totalPerYear: 'Итого / год',
|
||||
totalValue: 'Итого для роли: {{total}} ₽.',
|
||||
noPrice: '—',
|
||||
system: 'системная',
|
||||
edit: 'Изменить',
|
||||
save: 'Сохранить',
|
||||
@@ -671,6 +679,14 @@ const resources = {
|
||||
maxConfigsHint: '−1 = unlimited.',
|
||||
maxIpLimit: 'IP limit per config',
|
||||
maxIpLimitHint: '−1 = unlimited. Applies to new configs only.',
|
||||
pricePerConfigPerQuarter: 'Price per config / 3 months',
|
||||
pricePerConfigPerQuarterHint: 'Reference only, visible to admin only. Minimum billing period.',
|
||||
pricePerConfigPerYear: 'Price per config / year',
|
||||
pricePerConfigPerYearHint: 'Reference only, visible to admin only. Set independently from the quarterly price.',
|
||||
totalPerQuarter: 'Total / 3 months',
|
||||
totalPerYear: 'Total / year',
|
||||
totalValue: 'Total for role: {{total}} ₽.',
|
||||
noPrice: '—',
|
||||
system: 'system',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
|
||||
Reference in New Issue
Block a user