Implement pricing management functionality and update related components
- Added new endpoints for managing global pricing settings, including retrieval and updates for `PricePerConfigPerQuarter` and `PricePerConfigPerYear`. - Updated `RoleService` and related commands to remove pricing fields from role management, ensuring a clear separation between role configurations and global pricing. - Enhanced the `FactoryResetCommandHandler` to include seeding of pricing settings during a factory reset. - Modified frontend components to support new pricing settings, including forms for creating and updating pricing information. - Updated API documentation to reflect changes in pricing management endpoints and their expected request/response formats. - Adjusted tests to ensure proper coverage for new pricing functionalities and their integration with existing role management features.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { HttpError } from '@/shared/api/client'
|
||||
import type { PricingSettingsDto } from '@/shared/api/types'
|
||||
import { updatePricingSettings } from './api'
|
||||
|
||||
export function PricingSettingsEditor({ settings }: { settings: PricingSettingsDto }) {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [pricePerConfigPerQuarter, setPricePerConfigPerQuarter] = useState(
|
||||
settings.pricePerConfigPerQuarter != null ? String(settings.pricePerConfigPerQuarter) : '',
|
||||
)
|
||||
const [pricePerConfigPerYear, setPricePerConfigPerYear] = useState(
|
||||
settings.pricePerConfigPerYear != null ? String(settings.pricePerConfigPerYear) : '',
|
||||
)
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updatePricingSettings(
|
||||
pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter),
|
||||
pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear),
|
||||
),
|
||||
onSuccess: async () => {
|
||||
toast.success(t('admin.pricing.updated'))
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-pricing'] })
|
||||
},
|
||||
onError: (error) => toast.error(error instanceof HttpError ? error.detail : t('auth.genericError')),
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
mutation.mutate()
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="pricePerConfigPerQuarter">{t('admin.pricing.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.pricing.pricePerConfigPerQuarterHint')}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="pricePerConfigPerYear">{t('admin.pricing.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.pricing.pricePerConfigPerYearHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{t('admin.roles.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { apiRequest } from '@/shared/api/client'
|
||||
import type { PricingSettingsDto } from '@/shared/api/types'
|
||||
|
||||
export function getPricingSettings() {
|
||||
return apiRequest<PricingSettingsDto>('/admin/pricing')
|
||||
}
|
||||
|
||||
export function updatePricingSettings(pricePerConfigPerQuarter: number | null, pricePerConfigPerYear: number | null) {
|
||||
return apiRequest<PricingSettingsDto>('/admin/pricing', {
|
||||
method: 'PUT',
|
||||
body: { pricePerConfigPerQuarter, pricePerConfigPerYear },
|
||||
})
|
||||
}
|
||||
@@ -24,38 +24,17 @@ 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),
|
||||
pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter),
|
||||
pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear),
|
||||
)
|
||||
: createRole(
|
||||
name.trim(),
|
||||
Number(maxConfigs),
|
||||
Number(maxIpLimit),
|
||||
pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter),
|
||||
pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear),
|
||||
),
|
||||
? updateRole(role.id, Number(maxConfigs), Number(maxIpLimit))
|
||||
: createRole(name.trim(), Number(maxConfigs), Number(maxIpLimit)),
|
||||
onSuccess: async () => {
|
||||
toast.success(role ? t('admin.roles.updated') : t('admin.roles.created'))
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-roles'] })
|
||||
@@ -63,17 +42,10 @@ 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 && (
|
||||
@@ -108,40 +80,6 @@ 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,30 +5,12 @@ export function listRoles() {
|
||||
return apiRequest<RoleDto[]>('/admin/roles')
|
||||
}
|
||||
|
||||
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 createRole(name: string, maxConfigs: number, maxIpLimit: number) {
|
||||
return apiRequest<RoleDto>('/admin/roles', { method: 'POST', body: { name, 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 updateRole(id: string, maxConfigs: number, maxIpLimit: number) {
|
||||
return apiRequest<RoleDto>(`/admin/roles/${id}`, { method: 'PUT', body: { maxConfigs, maxIpLimit } })
|
||||
}
|
||||
|
||||
export function deleteRole(id: string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiRequest, apiUpload, getAccessToken } from '@/shared/api/client'
|
||||
import type {
|
||||
PagedList,
|
||||
SelectableRoleDto,
|
||||
RoleDto,
|
||||
TicketCommentDto,
|
||||
TicketDetailDto,
|
||||
TicketStatus,
|
||||
@@ -25,7 +25,7 @@ export function getTicket(id: string) {
|
||||
}
|
||||
|
||||
export function listSelectableRoles() {
|
||||
return apiRequest<SelectableRoleDto[]>('/support/roles')
|
||||
return apiRequest<RoleDto[]>('/support/roles')
|
||||
}
|
||||
|
||||
export function createBugReportTicket(message: string, files: File[]) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||
import { Route as AdminSupportRouteImport } from './routes/admin/support'
|
||||
import { Route as AdminRolesRouteImport } from './routes/admin/roles'
|
||||
import { Route as AdminPricingRouteImport } from './routes/admin/pricing'
|
||||
import { Route as AdminNodesRouteImport } from './routes/admin/nodes'
|
||||
import { Route as AdminNewsRouteImport } from './routes/admin/news'
|
||||
import { Route as AdminMaintenanceRouteImport } from './routes/admin/maintenance'
|
||||
@@ -96,6 +97,11 @@ const AdminRolesRoute = AdminRolesRouteImport.update({
|
||||
path: '/roles',
|
||||
getParentRoute: () => AdminRoute,
|
||||
} as any)
|
||||
const AdminPricingRoute = AdminPricingRouteImport.update({
|
||||
id: '/pricing',
|
||||
path: '/pricing',
|
||||
getParentRoute: () => AdminRoute,
|
||||
} as any)
|
||||
const AdminNodesRoute = AdminNodesRouteImport.update({
|
||||
id: '/nodes',
|
||||
path: '/nodes',
|
||||
@@ -155,6 +161,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/maintenance': typeof AdminMaintenanceRoute
|
||||
'/admin/news': typeof AdminNewsRoute
|
||||
'/admin/nodes': typeof AdminNodesRoute
|
||||
'/admin/pricing': typeof AdminPricingRoute
|
||||
'/admin/roles': typeof AdminRolesRoute
|
||||
'/admin/support': typeof AdminSupportRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
@@ -177,6 +184,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/maintenance': typeof AdminMaintenanceRoute
|
||||
'/admin/news': typeof AdminNewsRoute
|
||||
'/admin/nodes': typeof AdminNodesRoute
|
||||
'/admin/pricing': typeof AdminPricingRoute
|
||||
'/admin/roles': typeof AdminRolesRoute
|
||||
'/admin/support': typeof AdminSupportRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
@@ -201,6 +209,7 @@ export interface FileRoutesById {
|
||||
'/admin/maintenance': typeof AdminMaintenanceRoute
|
||||
'/admin/news': typeof AdminNewsRoute
|
||||
'/admin/nodes': typeof AdminNodesRoute
|
||||
'/admin/pricing': typeof AdminPricingRoute
|
||||
'/admin/roles': typeof AdminRolesRoute
|
||||
'/admin/support': typeof AdminSupportRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
@@ -226,6 +235,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/maintenance'
|
||||
| '/admin/news'
|
||||
| '/admin/nodes'
|
||||
| '/admin/pricing'
|
||||
| '/admin/roles'
|
||||
| '/admin/support'
|
||||
| '/admin/users'
|
||||
@@ -248,6 +258,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/maintenance'
|
||||
| '/admin/news'
|
||||
| '/admin/nodes'
|
||||
| '/admin/pricing'
|
||||
| '/admin/roles'
|
||||
| '/admin/support'
|
||||
| '/admin/users'
|
||||
@@ -271,6 +282,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/maintenance'
|
||||
| '/admin/news'
|
||||
| '/admin/nodes'
|
||||
| '/admin/pricing'
|
||||
| '/admin/roles'
|
||||
| '/admin/support'
|
||||
| '/admin/users'
|
||||
@@ -382,6 +394,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AdminRolesRouteImport
|
||||
parentRoute: typeof AdminRoute
|
||||
}
|
||||
'/admin/pricing': {
|
||||
id: '/admin/pricing'
|
||||
path: '/pricing'
|
||||
fullPath: '/admin/pricing'
|
||||
preLoaderRoute: typeof AdminPricingRouteImport
|
||||
parentRoute: typeof AdminRoute
|
||||
}
|
||||
'/admin/nodes': {
|
||||
id: '/admin/nodes'
|
||||
path: '/nodes'
|
||||
@@ -450,6 +469,7 @@ interface AdminRouteChildren {
|
||||
AdminMaintenanceRoute: typeof AdminMaintenanceRoute
|
||||
AdminNewsRoute: typeof AdminNewsRoute
|
||||
AdminNodesRoute: typeof AdminNodesRoute
|
||||
AdminPricingRoute: typeof AdminPricingRoute
|
||||
AdminRolesRoute: typeof AdminRolesRoute
|
||||
AdminSupportRoute: typeof AdminSupportRoute
|
||||
AdminUsersRoute: typeof AdminUsersRoute
|
||||
@@ -465,6 +485,7 @@ const AdminRouteChildren: AdminRouteChildren = {
|
||||
AdminMaintenanceRoute: AdminMaintenanceRoute,
|
||||
AdminNewsRoute: AdminNewsRoute,
|
||||
AdminNodesRoute: AdminNodesRoute,
|
||||
AdminPricingRoute: AdminPricingRoute,
|
||||
AdminRolesRoute: AdminRolesRoute,
|
||||
AdminSupportRoute: AdminSupportRoute,
|
||||
AdminUsersRoute: AdminUsersRoute,
|
||||
|
||||
@@ -11,6 +11,7 @@ const TABS = [
|
||||
{ to: '/admin/users', key: 'users' },
|
||||
{ to: '/admin/configs', key: 'configs' },
|
||||
{ to: '/admin/roles', key: 'roles' },
|
||||
{ to: '/admin/pricing', key: 'pricing' },
|
||||
{ to: '/admin/nodes', key: 'nodes' },
|
||||
{ to: '/admin/apps', key: 'apps' },
|
||||
{ to: '/admin/instructions', key: 'instructions' },
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { getPricingSettings } from '@/features/admin/pricing/api'
|
||||
import { PricingSettingsEditor } from '@/features/admin/pricing/PricingSettingsEditor'
|
||||
|
||||
export const Route = createFileRoute('/admin/pricing')({ component: AdminPricingPage })
|
||||
|
||||
function AdminPricingPage() {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['admin-pricing'], queryFn: getPricingSettings })
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('admin.pricing.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p className="text-sm text-muted-foreground">…</p>}
|
||||
|
||||
{isError && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">{t('auth.genericError')}</p>
|
||||
<Button variant="outline" size="sm" onClick={() => void refetch()}>
|
||||
{t('activation.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && <PricingSettingsEditor settings={data} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@/shared/ui/button'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { listRoles, deleteRole } from '@/features/admin/roles/api'
|
||||
import { RoleFormDialog } from '@/features/admin/roles/RoleFormDialog'
|
||||
import { getPricingSettings } from '@/features/admin/pricing/api'
|
||||
import type { RoleDto } from '@/shared/api/types'
|
||||
|
||||
export const Route = createFileRoute('/admin/roles')({ component: AdminRolesPage })
|
||||
@@ -17,8 +18,9 @@ function AdminRolesPage() {
|
||||
const [editing, setEditing] = useState<RoleDto | null>(null)
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['admin-roles'], queryFn: listRoles })
|
||||
const { data: pricing } = useQuery({ queryKey: ['admin-pricing'], queryFn: getPricingSettings })
|
||||
|
||||
const totalPrice = (price: number | null, maxConfigs: number) =>
|
||||
const totalPrice = (price: number | null | undefined, maxConfigs: number) =>
|
||||
price == null || maxConfigs < 0 ? t('admin.roles.noPrice') : `${price * maxConfigs} ₽`
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -69,8 +71,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">{totalPrice(pricing?.pricePerConfigPerQuarter, role.maxConfigs)}</td>
|
||||
<td className="py-2">{totalPrice(pricing?.pricePerConfigPerYear, role.maxConfigs)}</td>
|
||||
<td className="py-2 text-right">
|
||||
<Button size="sm" variant="outline" onClick={() => setEditing(role)}>
|
||||
{t('admin.roles.edit')}
|
||||
|
||||
@@ -2086,6 +2086,64 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/pricing": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PricingSettingsDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdatePricingSettingsCommand"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["PricingSettingsDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/support/roles": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2108,7 +2166,7 @@ export interface paths {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SelectableRoleDto"][];
|
||||
"application/json": components["schemas"]["RoleDto"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -3087,10 +3145,6 @@ export interface components {
|
||||
maxConfigs: number | string;
|
||||
/** Format: int32 */
|
||||
maxIpLimit: number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerQuarter: null | number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerYear: null | number | string;
|
||||
};
|
||||
CreateRoleRequestBody: {
|
||||
/** Format: uuid */
|
||||
@@ -3257,6 +3311,12 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
pageSize: number | string;
|
||||
};
|
||||
PricingSettingsDto: {
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerQuarter: null | number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerYear: null | number | string;
|
||||
};
|
||||
PublishInboundBody: {
|
||||
isPublished: boolean;
|
||||
displayName: null | string;
|
||||
@@ -3299,19 +3359,6 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
maxIpLimit: number | string;
|
||||
isSystem: boolean;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerQuarter?: null | number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerYear?: null | number | string;
|
||||
};
|
||||
SelectableRoleDto: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
name: string;
|
||||
/** Format: int32 */
|
||||
maxConfigs: number | string;
|
||||
/** Format: int32 */
|
||||
maxIpLimit: number | string;
|
||||
};
|
||||
StatsDto: {
|
||||
/** Format: int32 */
|
||||
@@ -3442,15 +3489,17 @@ export interface components {
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
UpdatePricingSettingsCommand: {
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerQuarter: null | number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerYear: null | number | string;
|
||||
};
|
||||
UpdateRoleBody: {
|
||||
/** Format: int32 */
|
||||
maxConfigs: number | string;
|
||||
/** Format: int32 */
|
||||
maxIpLimit: number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerQuarter: null | number | string;
|
||||
/** Format: int32 */
|
||||
pricePerConfigPerYear: null | number | string;
|
||||
};
|
||||
UserSummaryDto: {
|
||||
/** Format: uuid */
|
||||
|
||||
@@ -173,16 +173,12 @@ 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
|
||||
/** Глобальная справочная цена за конфиг (видна только админу) — одна на весь сервис, не per-роль. */
|
||||
export type PricingSettingsDto = {
|
||||
pricePerConfigPerQuarter: number | null
|
||||
pricePerConfigPerYear: number | null
|
||||
}
|
||||
|
||||
export type ActivationRequestAdminDto = {
|
||||
|
||||
@@ -191,6 +191,7 @@ const resources = {
|
||||
users: 'Пользователи',
|
||||
configs: 'Все конфиги',
|
||||
roles: 'Роли',
|
||||
pricing: 'Цены',
|
||||
nodes: 'Ноды',
|
||||
apps: 'Приложения',
|
||||
instructions: 'Инструкции',
|
||||
@@ -253,13 +254,8 @@ const resources = {
|
||||
maxConfigsHint: '−1 = без лимита.',
|
||||
maxIpLimit: 'Лимит IP на конфиг',
|
||||
maxIpLimitHint: '−1 = без лимита. Применяется только к новым конфигам.',
|
||||
pricePerConfigPerQuarter: 'Цена за конфиг / 3 мес',
|
||||
pricePerConfigPerQuarterHint: 'Справочно, видно только админу. Минимальный период оплаты.',
|
||||
pricePerConfigPerYear: 'Цена за конфиг / год',
|
||||
pricePerConfigPerYearHint: 'Справочно, видно только админу. Задаётся отдельно от квартальной цены.',
|
||||
totalPerQuarter: 'Итого / 3 мес',
|
||||
totalPerYear: 'Итого / год',
|
||||
totalValue: 'Итого для роли: {{total}} ₽.',
|
||||
noPrice: '—',
|
||||
system: 'системная',
|
||||
edit: 'Изменить',
|
||||
@@ -270,6 +266,14 @@ const resources = {
|
||||
updated: 'Квота обновлена.',
|
||||
deleted: 'Роль удалена.',
|
||||
},
|
||||
pricing: {
|
||||
title: 'Справочная цена конфига',
|
||||
pricePerConfigPerQuarter: 'Цена за конфиг / 3 мес',
|
||||
pricePerConfigPerQuarterHint: 'Минимальный период оплаты. Видно только админу.',
|
||||
pricePerConfigPerYear: 'Цена за конфиг / год',
|
||||
pricePerConfigPerYearHint: 'Задаётся независимо от квартальной цены (можно сделать скидку за год).',
|
||||
updated: 'Цена обновлена.',
|
||||
},
|
||||
nodes: {
|
||||
create: 'Добавить ноду',
|
||||
name: 'Название',
|
||||
@@ -617,6 +621,7 @@ const resources = {
|
||||
users: 'Users',
|
||||
configs: 'All configs',
|
||||
roles: 'Roles',
|
||||
pricing: 'Pricing',
|
||||
nodes: 'Nodes',
|
||||
apps: 'Apps',
|
||||
instructions: 'Instructions',
|
||||
@@ -679,13 +684,8 @@ 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',
|
||||
@@ -696,6 +696,14 @@ const resources = {
|
||||
updated: 'Quota updated.',
|
||||
deleted: 'Role deleted.',
|
||||
},
|
||||
pricing: {
|
||||
title: 'Reference config price',
|
||||
pricePerConfigPerQuarter: 'Price per config / 3 months',
|
||||
pricePerConfigPerQuarterHint: 'Minimum billing period. Visible to admin only.',
|
||||
pricePerConfigPerYear: 'Price per config / year',
|
||||
pricePerConfigPerYearHint: 'Set independently from the quarterly price (allows an annual discount).',
|
||||
updated: 'Pricing updated.',
|
||||
},
|
||||
nodes: {
|
||||
create: 'Add node',
|
||||
name: 'Name',
|
||||
|
||||
Reference in New Issue
Block a user