Refactor pricing endpoints and enhance support for pricing retrieval
- 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:
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.',
|
||||
|
||||
Reference in New Issue
Block a user