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:
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user