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' type TierRow = { minConfigs: string; discountPercent: string } export function PricingSettingsEditor({ settings }: { settings: PricingSettingsDto }) { const { t } = useTranslation() const queryClient = useQueryClient() const [pricePerConfigPerQuarter, setPricePerConfigPerQuarter] = useState( settings.pricePerConfigPerQuarter != null ? String(settings.pricePerConfigPerQuarter) : '', ) const [pricePerConfigPerHalfYear, setPricePerConfigPerHalfYear] = useState( settings.pricePerConfigPerHalfYear != null ? String(settings.pricePerConfigPerHalfYear) : '', ) const [pricePerConfigPerYear, setPricePerConfigPerYear] = useState( settings.pricePerConfigPerYear != null ? String(settings.pricePerConfigPerYear) : '', ) const [tiers, setTiers] = useState( settings.discountTiers.map((t) => ({ minConfigs: String(t.minConfigs), discountPercent: String(t.discountPercent) })), ) const updateTier = (index: number, patch: Partial) => setTiers((rows) => rows.map((row, i) => (i === index ? { ...row, ...patch } : row))) const removeTier = (index: number) => setTiers((rows) => rows.filter((_, i) => i !== index)) const addTier = () => setTiers((rows) => [...rows, { minConfigs: '', discountPercent: '' }]) // Зеркалит бэкенд-инварианты UpdatePricingSettingsCommandValidator: пороги не повторяются, скидка // на большем пороге не меньше скидки на меньшем (иначе лесенка не прогрессивная). const tierErrors = (() => { if (tiers.some((t) => t.minConfigs === '' || t.discountPercent === '')) return null const parsed = tiers.map((t) => ({ minConfigs: Number(t.minConfigs), discountPercent: Number(t.discountPercent) })) if (parsed.some((t) => !Number.isInteger(t.minConfigs) || t.minConfigs < 1)) return t('admin.pricing.discountTierInvalidMinConfigs') if (parsed.some((t) => !Number.isInteger(t.discountPercent) || t.discountPercent < 1 || t.discountPercent > 99)) return t('admin.pricing.discountTierInvalidPercent') if (new Set(parsed.map((t) => t.minConfigs)).size !== parsed.length) return t('admin.pricing.discountTiersDuplicate') const sorted = [...parsed].sort((a, b) => a.minConfigs - b.minConfigs) for (let i = 1; i < sorted.length; i++) { if (sorted[i].discountPercent < sorted[i - 1].discountPercent) return t('admin.pricing.discountTiersNotProgressive') } return null })() const hasIncompleteTier = tiers.some((t) => t.minConfigs === '' || t.discountPercent === '') // Все ставки — цена за конфиг в месяц; итог за более длинный период (ставка × месяцев) не должен // быть дешевле итога за более короткий, иначе выгоднее купить длинный тариф и не продлевать. const isHalfYearCheaperThanQuarter = pricePerConfigPerQuarter !== '' && pricePerConfigPerHalfYear !== '' && Number(pricePerConfigPerHalfYear) * 6 < Number(pricePerConfigPerQuarter) * 3 const isYearCheaperThanHalfYear = pricePerConfigPerHalfYear !== '' && pricePerConfigPerYear !== '' && Number(pricePerConfigPerYear) * 12 < Number(pricePerConfigPerHalfYear) * 6 const isYearCheaperThanQuarter = pricePerConfigPerHalfYear === '' && pricePerConfigPerQuarter !== '' && pricePerConfigPerYear !== '' && Number(pricePerConfigPerYear) * 12 < Number(pricePerConfigPerQuarter) * 3 const hasInvalidCombo = isHalfYearCheaperThanQuarter || isYearCheaperThanHalfYear || isYearCheaperThanQuarter const mutation = useMutation({ mutationFn: () => updatePricingSettings( pricePerConfigPerQuarter === '' ? null : Number(pricePerConfigPerQuarter), pricePerConfigPerHalfYear === '' ? null : Number(pricePerConfigPerHalfYear), pricePerConfigPerYear === '' ? null : Number(pricePerConfigPerYear), tiers.map((t) => ({ minConfigs: Number(t.minConfigs), discountPercent: Number(t.discountPercent) })), ), 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 (
{ e.preventDefault() mutation.mutate() }} >
setPricePerConfigPerQuarter(e.target.value)} />

{t('admin.pricing.pricePerConfigPerQuarterHint')}

setPricePerConfigPerHalfYear(e.target.value)} />

{t('admin.pricing.pricePerConfigPerHalfYearHint')}

{isHalfYearCheaperThanQuarter && (

{t('admin.pricing.halfYearCheaperThanQuarter')}

)}
setPricePerConfigPerYear(e.target.value)} />

{t('admin.pricing.pricePerConfigPerYearHint')}

{isYearCheaperThanHalfYear && (

{t('admin.pricing.yearCheaperThanHalfYear')}

)} {isYearCheaperThanQuarter && (

{t('admin.pricing.yearCheaperThanQuarter')}

)}

{t('admin.pricing.discountTiersHint')}

{tiers.map((tier, index) => (
updateTier(index, { minConfigs: e.target.value })} /> updateTier(index, { discountPercent: e.target.value })} />
))} {tierErrors &&

{tierErrors}

}
) }