Implement pricing management functionality and update related components
CI / Backend (build + test) (push) Successful in 1m19s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- 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:
Leonid Pershin
2026-07-18 19:34:29 +03:00
parent 285d8180c8
commit ae379f8e0f
54 changed files with 2516 additions and 268 deletions
+36
View File
@@ -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>
)
}
+5 -3
View File
@@ -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')}