Refactor pricing endpoints and enhance support for pricing retrieval
CI / Backend (build + test) (push) Successful in 1m21s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- Added a new endpoint `/api/support/pricing` to allow users to retrieve pricing information, making it accessible for role request dialogs.
- Introduced the `GetSupportPricing` method to handle pricing queries, ensuring that pricing data is available to non-admin users.
- Updated frontend components to integrate the new pricing retrieval functionality, displaying estimated costs based on user-selected configurations.
- Removed the `PricingSettingsDto` as it is no longer needed, streamlining the pricing data structure.
- Enhanced API documentation to reflect the new endpoint and its usage in the support context.
This commit is contained in:
Leonid Pershin
2026-07-18 21:19:40 +03:00
parent 32221af503
commit 3304eed4b3
14 changed files with 127 additions and 24 deletions
@@ -9,7 +9,7 @@ import { Label } from '@/shared/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/shared/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { HttpError } from '@/shared/api/client'
import { createRoleRequestTicket, listSelectableRoles } from './api'
import { createRoleRequestTicket, getSupportPricing, listSelectableRoles } from './api'
type Mode = 'existing' | 'new'
@@ -25,6 +25,7 @@ export function CreateRoleRequestDialog() {
const [justification, setJustification] = useState('')
const rolesQuery = useQuery({ queryKey: ['selectable-roles'], queryFn: listSelectableRoles, enabled: open })
const pricingQuery = useQuery({ queryKey: ['support-pricing'], queryFn: getSupportPricing, enabled: open })
const resetForm = () => {
setMode('existing')
@@ -70,6 +71,23 @@ export function CreateRoleRequestDialog() {
? roleId.length > 0
: newRoleName.trim().length > 0 && newRoleMaxConfigs !== '' && newRoleMaxIpLimit !== '')
// Квота, для которой считаем ориентировочную стоимость: у существующей роли — её maxConfigs,
// у новой — то, что пользователь ввёл (пока не введено или отрицательное кроме -1 — не считаем).
const maxConfigsForPricing =
mode === 'existing'
? rolesQuery.data?.find((role) => role.id === roleId)?.maxConfigs
: newRoleMaxConfigs !== '' && Number.isFinite(Number(newRoleMaxConfigs))
? Number(newRoleMaxConfigs)
: undefined
// Ставки — цена за конфиг В МЕСЯЦ; итог за период = ставка × месяцев × квота.
const totalPrice = (monthlyRate: number | null | undefined, months: number) =>
monthlyRate == null || maxConfigsForPricing == null || maxConfigsForPricing < 0
? t('admin.roles.noPrice')
: `${monthlyRate * months * maxConfigsForPricing}`
const showPricing = maxConfigsForPricing != null && maxConfigsForPricing >= 0
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@@ -146,6 +164,16 @@ export function CreateRoleRequestDialog() {
</>
)}
{showPricing && (
<div className="flex flex-col gap-1 rounded-md border border-border px-3 py-2 text-sm">
<p className="font-medium">{t('support.pricingTitle')}</p>
<p>{t('support.pricingQuarter', { price: totalPrice(pricingQuery.data?.pricePerConfigPerQuarter, 3) })}</p>
<p>{t('support.pricingHalfYear', { price: totalPrice(pricingQuery.data?.pricePerConfigPerHalfYear, 6) })}</p>
<p>{t('support.pricingYear', { price: totalPrice(pricingQuery.data?.pricePerConfigPerYear, 12) })}</p>
<p className="text-xs text-muted-foreground">{t('support.pricingDisclaimer')}</p>
</div>
)}
<div className="flex flex-col gap-1.5">
<Label htmlFor="justification">{t('support.justification')}</Label>
<Textarea id="justification" value={justification} onChange={(e) => setJustification(e.target.value)} required />
+5
View File
@@ -1,6 +1,7 @@
import { apiRequest, apiUpload, getAccessToken } from '@/shared/api/client'
import type {
PagedList,
PricingSettingsDto,
RoleDto,
TicketCommentDto,
TicketDetailDto,
@@ -28,6 +29,10 @@ export function listSelectableRoles() {
return apiRequest<RoleDto[]>('/support/roles')
}
export function getSupportPricing() {
return apiRequest<PricingSettingsDto>('/support/pricing')
}
export function createBugReportTicket(message: string, files: File[]) {
const formData = new FormData()
formData.set('message', message)
+10
View File
@@ -135,6 +135,11 @@ const resources = {
newRoleName: 'Название роли',
newRoleMaxConfigs: 'Количество конфигов (-1 — без лимита)',
newRoleMaxIpLimit: 'Количество IP (-1 — без лимита)',
pricingTitle: 'Ориентировочная стоимость',
pricingQuarter: '3 месяца: {{price}}',
pricingHalfYear: 'Полгода: {{price}}',
pricingYear: 'Год: {{price}}',
pricingDisclaimer: 'Цены на данный момент ознакомительные.',
justification: 'Обоснование',
ticketCreated: 'Обращение отправлено.',
roleRequestPending: 'У вас уже есть необработанная заявка на роль.',
@@ -571,6 +576,11 @@ const resources = {
newRoleName: 'Role name',
newRoleMaxConfigs: 'Max configs (-1 = unlimited)',
newRoleMaxIpLimit: 'Max IPs (-1 = unlimited)',
pricingTitle: 'Estimated cost',
pricingQuarter: '3 months: {{price}}',
pricingHalfYear: '6 months: {{price}}',
pricingYear: 'Year: {{price}}',
pricingDisclaimer: 'Prices are indicative only at this time.',
justification: 'Justification',
ticketCreated: 'Ticket submitted.',
roleRequestPending: 'You already have a pending role request.',