Files
bedolaga-cabinet/src/pages/AdminTariffs.tsx

1432 lines
66 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import {
tariffsApi,
TariffListItem,
TariffDetail,
TariffCreateRequest,
TariffUpdateRequest,
PeriodPrice,
ServerInfo
} from '../api/tariffs'
// Icons
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
const PlusIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
)
const EditIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>
)
const TrashIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
)
const CheckIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
)
const XIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
)
const InfinityIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25" />
</svg>
)
const CalendarIcon = () => (
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
)
const SunIcon = () => (
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
)
// Type selection modal
interface TariffTypeSelectProps {
onSelect: (isDaily: boolean) => void
onClose: () => void
}
function TariffTypeSelect({ onSelect, onClose }: TariffTypeSelectProps) {
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl w-full max-w-md overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-dark-700">
<h2 className="text-lg font-semibold text-dark-100">Выберите тип тарифа</h2>
<button onClick={onClose} className="p-1 hover:bg-dark-700 rounded-lg transition-colors">
<XIcon />
</button>
</div>
<div className="p-4 space-y-3">
<button
onClick={() => onSelect(false)}
className="w-full p-4 bg-dark-700 hover:bg-dark-600 rounded-xl transition-colors text-left group"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-accent-500/20 rounded-lg text-accent-400 group-hover:bg-accent-500/30">
<CalendarIcon />
</div>
<div>
<h3 className="font-medium text-dark-100">Периодный тариф</h3>
<p className="text-sm text-dark-400 mt-1">
Оплата за период (7, 30, 90 дней и т.д.). Произвольные периоды и цены.
</p>
</div>
</div>
</button>
<button
onClick={() => onSelect(true)}
className="w-full p-4 bg-dark-700 hover:bg-dark-600 rounded-xl transition-colors text-left group"
>
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-500/20 rounded-lg text-amber-400 group-hover:bg-amber-500/30">
<SunIcon />
</div>
<div>
<h3 className="font-medium text-dark-100">Суточный тариф</h3>
<p className="text-sm text-dark-400 mt-1">
Ежедневное списание с баланса. Можно ставить на паузу.
</p>
</div>
</div>
</button>
</div>
</div>
</div>
)
}
// Period tariff modal
interface PeriodTariffModalProps {
tariff?: TariffDetail | null
servers: ServerInfo[]
onSave: (data: TariffCreateRequest | TariffUpdateRequest) => void
onClose: () => void
isLoading?: boolean
}
function PeriodTariffModal({ tariff, servers, onSave, onClose, isLoading }: PeriodTariffModalProps) {
const isEdit = !!tariff
const [name, setName] = useState(tariff?.name || '')
const [description, setDescription] = useState(tariff?.description || '')
const [trafficLimitGb, setTrafficLimitGb] = useState(tariff?.traffic_limit_gb || 100)
const [deviceLimit, setDeviceLimit] = useState(tariff?.device_limit || 1)
const [devicePriceKopeks, setDevicePriceKopeks] = useState(tariff?.device_price_kopeks || 0)
const [maxDeviceLimit, setMaxDeviceLimit] = useState(tariff?.max_device_limit || 0)
const [tierLevel, setTierLevel] = useState(tariff?.tier_level || 1)
const [periodPrices, setPeriodPrices] = useState<PeriodPrice[]>(
tariff?.period_prices?.length ? tariff.period_prices : []
)
const [selectedSquads, setSelectedSquads] = useState<string[]>(tariff?.allowed_squads || [])
// Докупка трафика
const [trafficTopupEnabled, setTrafficTopupEnabled] = useState(tariff?.traffic_topup_enabled || false)
const [maxTopupTrafficGb, setMaxTopupTrafficGb] = useState(tariff?.max_topup_traffic_gb || 0)
const [trafficTopupPackages, setTrafficTopupPackages] = useState<Record<string, number>>(
tariff?.traffic_topup_packages || {}
)
// Режим сброса трафика
const [trafficResetMode, setTrafficResetMode] = useState<string | null>(tariff?.traffic_reset_mode || null)
// Плавающий тариф - произвольное количество дней
const [customDaysEnabled, setCustomDaysEnabled] = useState(tariff?.custom_days_enabled || false)
const [pricePerDayKopeks, setPricePerDayKopeks] = useState(tariff?.price_per_day_kopeks || 0)
const [minDays, setMinDays] = useState(tariff?.min_days || 1)
const [maxDays, setMaxDays] = useState(tariff?.max_days || 365)
// Плавающий тариф - произвольный трафик
const [customTrafficEnabled, setCustomTrafficEnabled] = useState(tariff?.custom_traffic_enabled || false)
const [trafficPricePerGbKopeks, setTrafficPricePerGbKopeks] = useState(tariff?.traffic_price_per_gb_kopeks || 0)
const [minTrafficGb, setMinTrafficGb] = useState(tariff?.min_traffic_gb || 1)
const [maxTrafficGb, setMaxTrafficGb] = useState(tariff?.max_traffic_gb || 1000)
// Новый период для добавления
const [newPeriodDays, setNewPeriodDays] = useState(30)
const [newPeriodPrice, setNewPeriodPrice] = useState(300)
const [activeTab, setActiveTab] = useState<'basic' | 'periods' | 'servers' | 'extra'>('basic')
const handleSubmit = () => {
const data: TariffCreateRequest | TariffUpdateRequest = {
name,
description: description || undefined,
traffic_limit_gb: trafficLimitGb,
device_limit: deviceLimit,
device_price_kopeks: devicePriceKopeks > 0 ? devicePriceKopeks : undefined,
max_device_limit: maxDeviceLimit > 0 ? maxDeviceLimit : undefined,
tier_level: tierLevel,
period_prices: periodPrices.filter(p => p.price_kopeks > 0),
allowed_squads: selectedSquads,
traffic_topup_enabled: trafficTopupEnabled,
traffic_topup_packages: trafficTopupPackages,
max_topup_traffic_gb: maxTopupTrafficGb,
is_daily: false,
daily_price_kopeks: 0,
traffic_reset_mode: trafficResetMode,
// Плавающий тариф
custom_days_enabled: customDaysEnabled,
price_per_day_kopeks: pricePerDayKopeks,
min_days: minDays,
max_days: maxDays,
custom_traffic_enabled: customTrafficEnabled,
traffic_price_per_gb_kopeks: trafficPricePerGbKopeks,
min_traffic_gb: minTrafficGb,
max_traffic_gb: maxTrafficGb,
}
onSave(data)
}
const toggleServer = (uuid: string) => {
setSelectedSquads(prev =>
prev.includes(uuid)
? prev.filter(s => s !== uuid)
: [...prev, uuid]
)
}
const addPeriod = () => {
if (newPeriodDays > 0 && newPeriodPrice > 0) {
// Проверяем, нет ли уже такого периода
const exists = periodPrices.some(p => p.days === newPeriodDays)
if (!exists) {
setPeriodPrices(prev => [...prev, { days: newPeriodDays, price_kopeks: newPeriodPrice * 100 }].sort((a, b) => a.days - b.days))
setNewPeriodDays(30)
setNewPeriodPrice(300)
}
}
}
const removePeriod = (days: number) => {
setPeriodPrices(prev => prev.filter(p => p.days !== days))
}
const updatePeriodPrice = (days: number, priceRubles: number) => {
setPeriodPrices(prev => prev.map(p =>
p.days === days ? { ...p, price_kopeks: priceRubles * 100 } : p
))
}
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-dark-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-accent-500/20 rounded-lg text-accent-400">
<CalendarIcon />
</div>
<div>
<h2 className="text-lg font-semibold text-dark-100">
{isEdit ? 'Редактирование тарифа' : 'Новый периодный тариф'}
</h2>
<p className="text-xs text-dark-500">Оплата за период</p>
</div>
</div>
<button onClick={onClose} className="p-1 hover:bg-dark-700 rounded-lg transition-colors">
<XIcon />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-dark-700">
{(['basic', 'periods', 'servers', 'extra'] as const).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === tab
? 'text-accent-400 border-b-2 border-accent-400'
: 'text-dark-400 hover:text-dark-200'
}`}
>
{tab === 'basic' && 'Основное'}
{tab === 'periods' && 'Периоды'}
{tab === 'servers' && 'Серверы'}
{tab === 'extra' && 'Дополнительно'}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{activeTab === 'basic' && (
<div className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm text-dark-300 mb-1">Название тарифа</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
placeholder="Например: Стандарт"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm text-dark-300 mb-1">Описание</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500 resize-none"
rows={2}
placeholder="Краткое описание тарифа"
/>
</div>
{/* Traffic Limit */}
<div>
<label className="block text-sm text-dark-300 mb-1">Лимит трафика</label>
<div className="flex items-center gap-2">
<input
type="number"
value={trafficLimitGb}
onChange={e => setTrafficLimitGb(Math.max(0, parseInt(e.target.value) || 0))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
/>
<span className="text-dark-400">ГБ</span>
{trafficLimitGb === 0 && (
<span className="flex items-center gap-1 text-sm text-success-500">
<InfinityIcon />
Безлимит
</span>
)}
</div>
<p className="text-xs text-dark-500 mt-1">0 = безлимитный трафик</p>
</div>
{/* Device Limit */}
<div>
<label className="block text-sm text-dark-300 mb-1">Устройств в тарифе</label>
<input
type="number"
value={deviceLimit}
onChange={e => setDeviceLimit(Math.max(1, parseInt(e.target.value) || 1))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
{/* Tier Level */}
<div>
<label className="block text-sm text-dark-300 mb-1">Уровень тарифа</label>
<input
type="number"
value={tierLevel}
onChange={e => setTierLevel(Math.min(10, Math.max(1, parseInt(e.target.value) || 1)))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
max={10}
/>
<p className="text-xs text-dark-500 mt-1">Влияет на доступность перехода между тарифами</p>
</div>
</div>
)}
{activeTab === 'periods' && (
<div className="space-y-4">
<p className="text-sm text-dark-400">
Добавьте периоды и цены для тарифа. Пользователи смогут выбирать из добавленных периодов.
</p>
{/* Add new period */}
<div className="p-4 bg-dark-700/50 rounded-lg border border-dashed border-dark-600">
<h4 className="text-sm font-medium text-dark-300 mb-3">Добавить период</h4>
<div className="flex items-end gap-3 flex-wrap">
<div>
<label className="block text-xs text-dark-500 mb-1">Дней</label>
<input
type="number"
value={newPeriodDays}
onChange={e => setNewPeriodDays(Math.max(1, parseInt(e.target.value) || 1))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
<div>
<label className="block text-xs text-dark-500 mb-1">Цена ()</label>
<input
type="number"
value={newPeriodPrice}
onChange={e => setNewPeriodPrice(Math.max(1, parseInt(e.target.value) || 1))}
className="w-28 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
<button
onClick={addPeriod}
disabled={periodPrices.some(p => p.days === newPeriodDays)}
className="px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<PlusIcon />
Добавить
</button>
</div>
</div>
{/* Period list */}
{periodPrices.length === 0 ? (
<div className="text-center py-8 text-dark-500">
Нет добавленных периодов. Добавьте хотя бы один период.
</div>
) : (
<div className="space-y-2">
{periodPrices.map(period => (
<div
key={period.days}
className="flex items-center gap-3 p-3 bg-dark-700/50 rounded-lg"
>
<div className="w-20 text-dark-300 font-medium">{period.days} дн.</div>
<input
type="number"
value={period.price_kopeks / 100}
onChange={e => updatePeriodPrice(period.days, Math.max(0, parseFloat(e.target.value) || 0))}
className="w-28 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
step={1}
/>
<span className="text-dark-400"></span>
<div className="flex-1" />
<button
onClick={() => removePeriod(period.days)}
className="p-2 text-dark-400 hover:text-error-400 hover:bg-error-500/20 rounded-lg transition-colors"
>
<TrashIcon />
</button>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'servers' && (
<div className="space-y-2">
<p className="text-sm text-dark-400 mb-4">
Выберите серверы, доступные на этом тарифе.
</p>
{servers.length === 0 ? (
<p className="text-dark-500 text-center py-4">Нет доступных серверов</p>
) : (
servers.map(server => {
const isSelected = selectedSquads.includes(server.squad_uuid)
return (
<div
key={server.id}
onClick={() => toggleServer(server.squad_uuid)}
className={`p-3 rounded-lg transition-colors cursor-pointer ${
isSelected
? 'bg-accent-500/20 border border-accent-500/50'
: 'bg-dark-700 hover:bg-dark-600 border border-transparent'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-5 h-5 rounded flex items-center justify-center ${
isSelected
? 'bg-accent-500 text-white'
: 'bg-dark-600'
}`}>
{isSelected && <CheckIcon />}
</div>
<span className="text-dark-200 flex-1">{server.display_name}</span>
{server.country_code && (
<span className="text-xs text-dark-500">{server.country_code}</span>
)}
</div>
</div>
)
})
)}
</div>
)}
{activeTab === 'extra' && (
<div className="space-y-6">
{/* Докупка устройств */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<h4 className="text-sm font-medium text-dark-200 mb-3">Докупка устройств</h4>
<div className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-48">Цена за устройство (30 дней):</span>
<input
type="number"
value={devicePriceKopeks / 100}
onChange={e => setDevicePriceKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
step={1}
/>
<span className="text-dark-400"></span>
</div>
<p className="text-xs text-dark-500">0 = докупка недоступна</p>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-48">Макс. устройств на тарифе:</span>
<input
type="number"
value={maxDeviceLimit}
onChange={e => setMaxDeviceLimit(Math.max(0, parseInt(e.target.value) || 0))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
/>
</div>
<p className="text-xs text-dark-500">0 = без ограничений</p>
</div>
</div>
{/* Докупка трафика */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-dark-200">Докупка трафика</h4>
<button
type="button"
onClick={() => setTrafficTopupEnabled(!trafficTopupEnabled)}
className={`w-10 h-6 rounded-full transition-colors relative ${
trafficTopupEnabled ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
trafficTopupEnabled ? 'left-5' : 'left-1'
}`}
/>
</button>
</div>
{trafficTopupEnabled && (
<div className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Макс. лимит:</span>
<input
type="number"
value={maxTopupTrafficGb}
onChange={e => setMaxTopupTrafficGb(Math.max(0, parseInt(e.target.value) || 0))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
/>
<span className="text-dark-400">ГБ</span>
<span className="text-xs text-dark-500">(0 = без ограничений)</span>
</div>
<div className="mt-3">
<span className="text-sm text-dark-400">Пакеты трафика:</span>
<div className="mt-2 grid grid-cols-2 gap-2">
{[5, 10, 20, 50].map(gb => (
<div key={gb} className="flex items-center gap-2">
<span className="text-sm text-dark-300 w-12">{gb} ГБ:</span>
<input
type="number"
value={(trafficTopupPackages[String(gb)] || 0) / 100}
onChange={e => {
const price = Math.max(0, parseFloat(e.target.value) || 0) * 100
setTrafficTopupPackages(prev => ({
...prev,
[String(gb)]: price
}))
}}
className="w-20 px-2 py-1 bg-dark-600 border border-dark-500 rounded text-sm text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
step={1}
/>
<span className="text-xs text-dark-400"></span>
</div>
))}
</div>
</div>
</div>
)}
</div>
{/* Плавающий тариф - произвольное количество дней */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="text-sm font-medium text-dark-200">Произвольное количество дней</h4>
<p className="text-xs text-dark-500 mt-1">Пользователь сам выбирает срок подписки</p>
</div>
<button
type="button"
onClick={() => setCustomDaysEnabled(!customDaysEnabled)}
className={`w-10 h-6 rounded-full transition-colors relative ${
customDaysEnabled ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
customDaysEnabled ? 'left-5' : 'left-1'
}`}
/>
</button>
</div>
{customDaysEnabled && (
<div className="space-y-3 pt-2 border-t border-dark-600">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Цена за день:</span>
<input
type="number"
value={pricePerDayKopeks / 100}
onChange={e => setPricePerDayKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
step={0.1}
/>
<span className="text-dark-400"></span>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Мин. дней:</span>
<input
type="number"
value={minDays}
onChange={e => setMinDays(Math.max(1, parseInt(e.target.value) || 1))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Макс. дней:</span>
<input
type="number"
value={maxDays}
onChange={e => setMaxDays(Math.max(1, parseInt(e.target.value) || 1))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
</div>
)}
</div>
{/* Плавающий тариф - произвольный трафик */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="text-sm font-medium text-dark-200">Произвольный объём трафика</h4>
<p className="text-xs text-dark-500 mt-1">Пользователь сам выбирает объём трафика при покупке</p>
</div>
<button
type="button"
onClick={() => setCustomTrafficEnabled(!customTrafficEnabled)}
className={`w-10 h-6 rounded-full transition-colors relative ${
customTrafficEnabled ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
customTrafficEnabled ? 'left-5' : 'left-1'
}`}
/>
</button>
</div>
{customTrafficEnabled && (
<div className="space-y-3 pt-2 border-t border-dark-600">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Цена за 1 ГБ:</span>
<input
type="number"
value={trafficPricePerGbKopeks / 100}
onChange={e => setTrafficPricePerGbKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={0}
step={0.1}
/>
<span className="text-dark-400"></span>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Мин. ГБ:</span>
<input
type="number"
value={minTrafficGb}
onChange={e => setMinTrafficGb(Math.max(1, parseInt(e.target.value) || 1))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Макс. ГБ:</span>
<input
type="number"
value={maxTrafficGb}
onChange={e => setMaxTrafficGb(Math.max(1, parseInt(e.target.value) || 1))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500"
min={1}
/>
</div>
</div>
)}
</div>
{/* Режим сброса трафика */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<h4 className="text-sm font-medium text-dark-200 mb-3">Режим сброса трафика</h4>
<p className="text-xs text-dark-500 mb-3">
Определяет, когда сбрасывается использованный трафик у подписчиков этого тарифа
</p>
<div className="space-y-2">
{[
{ value: null, label: '🌐 Глобальная настройка', desc: 'Использовать значение из конфига бота' },
{ value: 'DAY', label: '📅 Ежедневно', desc: 'Сброс каждый день' },
{ value: 'WEEK', label: '📆 Еженедельно', desc: 'Сброс каждую неделю' },
{ value: 'MONTH', label: '🗓️ Ежемесячно', desc: 'Сброс каждый месяц' },
{ value: 'NO_RESET', label: '🚫 Никогда', desc: 'Трафик не сбрасывается' },
].map(option => (
<button
key={option.value || 'global'}
type="button"
onClick={() => setTrafficResetMode(option.value)}
className={`w-full p-3 rounded-lg text-left transition-colors ${
trafficResetMode === option.value
? 'bg-accent-500/20 border border-accent-500'
: 'bg-dark-600 border border-dark-500 hover:border-dark-400'
}`}
>
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-dark-100">{option.label}</span>
<p className="text-xs text-dark-400 mt-0.5">{option.desc}</p>
</div>
{trafficResetMode === option.value && (
<span className="text-accent-400"><CheckIcon /></span>
)}
</div>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-4 border-t border-dark-700">
<button
onClick={onClose}
className="px-4 py-2 text-dark-300 hover:text-dark-100 transition-colors"
>
Отмена
</button>
<button
onClick={handleSubmit}
disabled={!name || (periodPrices.length === 0 && !customDaysEnabled) || isLoading}
className="px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</div>
</div>
)
}
// Daily tariff modal
interface DailyTariffModalProps {
tariff?: TariffDetail | null
servers: ServerInfo[]
onSave: (data: TariffCreateRequest | TariffUpdateRequest) => void
onClose: () => void
isLoading?: boolean
}
function DailyTariffModal({ tariff, servers, onSave, onClose, isLoading }: DailyTariffModalProps) {
const isEdit = !!tariff
const [name, setName] = useState(tariff?.name || '')
const [description, setDescription] = useState(tariff?.description || '')
const [trafficLimitGb, setTrafficLimitGb] = useState(tariff?.traffic_limit_gb || 100)
const [deviceLimit, setDeviceLimit] = useState(tariff?.device_limit || 1)
const [devicePriceKopeks, setDevicePriceKopeks] = useState(tariff?.device_price_kopeks || 0)
const [maxDeviceLimit, setMaxDeviceLimit] = useState(tariff?.max_device_limit || 0)
const [tierLevel, setTierLevel] = useState(tariff?.tier_level || 1)
const [dailyPriceKopeks, setDailyPriceKopeks] = useState(tariff?.daily_price_kopeks || 0)
const [selectedSquads, setSelectedSquads] = useState<string[]>(tariff?.allowed_squads || [])
// Докупка трафика
const [trafficTopupEnabled, setTrafficTopupEnabled] = useState(tariff?.traffic_topup_enabled || false)
const [maxTopupTrafficGb, setMaxTopupTrafficGb] = useState(tariff?.max_topup_traffic_gb || 0)
const [trafficTopupPackages, setTrafficTopupPackages] = useState<Record<string, number>>(
tariff?.traffic_topup_packages || {}
)
// Режим сброса трафика
const [trafficResetMode, setTrafficResetMode] = useState<string | null>(tariff?.traffic_reset_mode || null)
const [activeTab, setActiveTab] = useState<'basic' | 'servers' | 'extra'>('basic')
const handleSubmit = () => {
const data: TariffCreateRequest | TariffUpdateRequest = {
name,
description: description || undefined,
traffic_limit_gb: trafficLimitGb,
device_limit: deviceLimit,
device_price_kopeks: devicePriceKopeks > 0 ? devicePriceKopeks : undefined,
max_device_limit: maxDeviceLimit > 0 ? maxDeviceLimit : undefined,
tier_level: tierLevel,
period_prices: [],
allowed_squads: selectedSquads,
traffic_topup_enabled: trafficTopupEnabled,
traffic_topup_packages: trafficTopupPackages,
max_topup_traffic_gb: maxTopupTrafficGb,
is_daily: true,
daily_price_kopeks: dailyPriceKopeks,
traffic_reset_mode: trafficResetMode,
}
onSave(data)
}
const toggleServer = (uuid: string) => {
setSelectedSquads(prev =>
prev.includes(uuid)
? prev.filter(s => s !== uuid)
: [...prev, uuid]
)
}
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-dark-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-500/20 rounded-lg text-amber-400">
<SunIcon />
</div>
<div>
<h2 className="text-lg font-semibold text-dark-100">
{isEdit ? 'Редактирование тарифа' : 'Новый суточный тариф'}
</h2>
<p className="text-xs text-dark-500">Ежедневное списание</p>
</div>
</div>
<button onClick={onClose} className="p-1 hover:bg-dark-700 rounded-lg transition-colors">
<XIcon />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-dark-700">
{(['basic', 'servers', 'extra'] as const).map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === tab
? 'text-amber-400 border-b-2 border-amber-400'
: 'text-dark-400 hover:text-dark-200'
}`}
>
{tab === 'basic' && 'Основное'}
{tab === 'servers' && 'Серверы'}
{tab === 'extra' && 'Дополнительно'}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{activeTab === 'basic' && (
<div className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm text-dark-300 mb-1">Название тарифа</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
placeholder="Например: Суточный"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm text-dark-300 mb-1">Описание</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500 resize-none"
rows={2}
placeholder="Краткое описание тарифа"
/>
</div>
{/* Daily Price */}
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<label className="block text-sm text-amber-400 font-medium mb-2">Цена за день</label>
<div className="flex items-center gap-2">
<input
type="number"
value={dailyPriceKopeks / 100}
onChange={e => setDailyPriceKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
step={0.1}
/>
<span className="text-dark-400">/день</span>
</div>
<p className="text-xs text-dark-500 mt-2">Списывается ежедневно с баланса пользователя</p>
</div>
{/* Traffic Limit */}
<div>
<label className="block text-sm text-dark-300 mb-1">Лимит трафика</label>
<div className="flex items-center gap-2">
<input
type="number"
value={trafficLimitGb}
onChange={e => setTrafficLimitGb(Math.max(0, parseInt(e.target.value) || 0))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
/>
<span className="text-dark-400">ГБ</span>
{trafficLimitGb === 0 && (
<span className="flex items-center gap-1 text-sm text-success-500">
<InfinityIcon />
Безлимит
</span>
)}
</div>
</div>
{/* Device Limit */}
<div>
<label className="block text-sm text-dark-300 mb-1">Устройств в тарифе</label>
<input
type="number"
value={deviceLimit}
onChange={e => setDeviceLimit(Math.max(1, parseInt(e.target.value) || 1))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={1}
/>
</div>
{/* Tier Level */}
<div>
<label className="block text-sm text-dark-300 mb-1">Уровень тарифа</label>
<input
type="number"
value={tierLevel}
onChange={e => setTierLevel(Math.min(10, Math.max(1, parseInt(e.target.value) || 1)))}
className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={1}
max={10}
/>
</div>
</div>
)}
{activeTab === 'servers' && (
<div className="space-y-2">
<p className="text-sm text-dark-400 mb-4">
Выберите серверы, доступные на этом тарифе.
</p>
{servers.length === 0 ? (
<p className="text-dark-500 text-center py-4">Нет доступных серверов</p>
) : (
servers.map(server => {
const isSelected = selectedSquads.includes(server.squad_uuid)
return (
<div
key={server.id}
onClick={() => toggleServer(server.squad_uuid)}
className={`p-3 rounded-lg transition-colors cursor-pointer ${
isSelected
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-dark-700 hover:bg-dark-600 border border-transparent'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-5 h-5 rounded flex items-center justify-center ${
isSelected
? 'bg-amber-500 text-white'
: 'bg-dark-600'
}`}>
{isSelected && <CheckIcon />}
</div>
<span className="text-dark-200 flex-1">{server.display_name}</span>
{server.country_code && (
<span className="text-xs text-dark-500">{server.country_code}</span>
)}
</div>
</div>
)
})
)}
</div>
)}
{activeTab === 'extra' && (
<div className="space-y-6">
{/* Докупка устройств */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<h4 className="text-sm font-medium text-dark-200 mb-3">Докупка устройств</h4>
<div className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-48">Цена за устройство (30 дней):</span>
<input
type="number"
value={devicePriceKopeks / 100}
onChange={e => setDevicePriceKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
step={1}
/>
<span className="text-dark-400"></span>
</div>
<p className="text-xs text-dark-500">0 = докупка недоступна</p>
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-48">Макс. устройств на тарифе:</span>
<input
type="number"
value={maxDeviceLimit}
onChange={e => setMaxDeviceLimit(Math.max(0, parseInt(e.target.value) || 0))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
/>
</div>
<p className="text-xs text-dark-500">0 = без ограничений</p>
</div>
</div>
{/* Докупка трафика */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-dark-200">Докупка трафика</h4>
<button
type="button"
onClick={() => setTrafficTopupEnabled(!trafficTopupEnabled)}
className={`w-10 h-6 rounded-full transition-colors relative ${
trafficTopupEnabled ? 'bg-amber-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
trafficTopupEnabled ? 'left-5' : 'left-1'
}`}
/>
</button>
</div>
{trafficTopupEnabled && (
<div className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-sm text-dark-400 w-32">Макс. лимит:</span>
<input
type="number"
value={maxTopupTrafficGb}
onChange={e => setMaxTopupTrafficGb(Math.max(0, parseInt(e.target.value) || 0))}
className="w-24 px-3 py-2 bg-dark-600 border border-dark-500 rounded-lg text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
/>
<span className="text-dark-400">ГБ</span>
<span className="text-xs text-dark-500">(0 = без ограничений)</span>
</div>
<div className="mt-3">
<span className="text-sm text-dark-400">Пакеты трафика:</span>
<div className="mt-2 grid grid-cols-2 gap-2">
{[5, 10, 20, 50].map(gb => (
<div key={gb} className="flex items-center gap-2">
<span className="text-sm text-dark-300 w-12">{gb} ГБ:</span>
<input
type="number"
value={(trafficTopupPackages[String(gb)] || 0) / 100}
onChange={e => {
const price = Math.max(0, parseFloat(e.target.value) || 0) * 100
setTrafficTopupPackages(prev => ({
...prev,
[String(gb)]: price
}))
}}
className="w-20 px-2 py-1 bg-dark-600 border border-dark-500 rounded text-sm text-dark-100 focus:outline-none focus:border-amber-500"
min={0}
step={1}
/>
<span className="text-xs text-dark-400"></span>
</div>
))}
</div>
</div>
</div>
)}
</div>
{/* Режим сброса трафика */}
<div className="p-4 bg-dark-700/50 rounded-lg">
<h4 className="text-sm font-medium text-dark-200 mb-3">Режим сброса трафика</h4>
<p className="text-xs text-dark-500 mb-3">
Определяет, когда сбрасывается использованный трафик у подписчиков этого тарифа
</p>
<div className="space-y-2">
{[
{ value: null, label: '🌐 Глобальная настройка', desc: 'Использовать значение из конфига бота' },
{ value: 'DAY', label: '📅 Ежедневно', desc: 'Сброс каждый день' },
{ value: 'WEEK', label: '📆 Еженедельно', desc: 'Сброс каждую неделю' },
{ value: 'MONTH', label: '🗓️ Ежемесячно', desc: 'Сброс каждый месяц' },
{ value: 'NO_RESET', label: '🚫 Никогда', desc: 'Трафик не сбрасывается' },
].map(option => (
<button
key={option.value || 'global'}
type="button"
onClick={() => setTrafficResetMode(option.value)}
className={`w-full p-3 rounded-lg text-left transition-colors ${
trafficResetMode === option.value
? 'bg-amber-500/20 border border-amber-500'
: 'bg-dark-600 border border-dark-500 hover:border-dark-400'
}`}
>
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-dark-100">{option.label}</span>
<p className="text-xs text-dark-400 mt-0.5">{option.desc}</p>
</div>
{trafficResetMode === option.value && (
<span className="text-amber-400"><CheckIcon /></span>
)}
</div>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-4 border-t border-dark-700">
<button
onClick={onClose}
className="px-4 py-2 text-dark-300 hover:text-dark-100 transition-colors"
>
Отмена
</button>
<button
onClick={handleSubmit}
disabled={!name || dailyPriceKopeks <= 0 || isLoading}
className="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</div>
</div>
)
}
export default function AdminTariffs() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [showTypeSelect, setShowTypeSelect] = useState(false)
const [showPeriodModal, setShowPeriodModal] = useState(false)
const [showDailyModal, setShowDailyModal] = useState(false)
const [editingTariff, setEditingTariff] = useState<TariffDetail | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null)
// Queries
const { data: tariffsData, isLoading } = useQuery({
queryKey: ['admin-tariffs'],
queryFn: () => tariffsApi.getTariffs(true),
})
const { data: servers = [] } = useQuery({
queryKey: ['admin-tariffs-servers'],
queryFn: () => tariffsApi.getAvailableServers(),
})
// Mutations
const createMutation = useMutation({
mutationFn: tariffsApi.createTariff,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] })
setShowPeriodModal(false)
setShowDailyModal(false)
},
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: TariffUpdateRequest }) =>
tariffsApi.updateTariff(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] })
setShowPeriodModal(false)
setShowDailyModal(false)
setEditingTariff(null)
},
})
const deleteMutation = useMutation({
mutationFn: tariffsApi.deleteTariff,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] })
setDeleteConfirm(null)
},
})
const toggleMutation = useMutation({
mutationFn: tariffsApi.toggleTariff,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] })
},
})
const toggleTrialMutation = useMutation({
mutationFn: tariffsApi.toggleTrial,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] })
},
})
const handleTypeSelect = (isDaily: boolean) => {
setShowTypeSelect(false)
if (isDaily) {
setShowDailyModal(true)
} else {
setShowPeriodModal(true)
}
}
const handleEdit = async (tariffId: number) => {
try {
const detail = await tariffsApi.getTariff(tariffId)
setEditingTariff(detail)
// Открываем соответствующую модалку в зависимости от типа тарифа
if (detail.is_daily) {
setShowDailyModal(true)
} else {
setShowPeriodModal(true)
}
} catch (error) {
console.error('Failed to load tariff:', error)
}
}
const handleSave = (data: TariffCreateRequest | TariffUpdateRequest) => {
if (editingTariff) {
updateMutation.mutate({ id: editingTariff.id, data })
} else {
createMutation.mutate(data as TariffCreateRequest)
}
}
const handleCloseModal = () => {
setShowPeriodModal(false)
setShowDailyModal(false)
setEditingTariff(null)
}
const tariffs = tariffsData?.tariffs || []
return (
<div className="animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Link
to="/admin"
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
>
<BackIcon />
</Link>
<div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
</div>
</div>
<button
onClick={() => { setEditingTariff(null); setShowTypeSelect(true) }}
className="flex items-center gap-2 px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-colors"
>
<PlusIcon />
{t('admin.tariffs.create')}
</button>
</div>
{/* Tariffs List */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
</div>
) : tariffs.length === 0 ? (
<div className="text-center py-12">
<p className="text-dark-400">{t('admin.tariffs.noTariffs')}</p>
</div>
) : (
<div className="space-y-3">
{tariffs.map((tariff: TariffListItem) => (
<div
key={tariff.id}
className={`p-4 bg-dark-800 rounded-xl border transition-colors ${
tariff.is_active ? 'border-dark-700' : 'border-dark-700/50 opacity-60'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-medium text-dark-100 truncate">{tariff.name}</h3>
{tariff.is_daily ? (
<span className="px-2 py-0.5 text-xs bg-amber-500/20 text-amber-400 rounded">
Суточный
</span>
) : (
<span className="px-2 py-0.5 text-xs bg-accent-500/20 text-accent-400 rounded">
Периодный
</span>
)}
{tariff.is_trial_available && (
<span className="px-2 py-0.5 text-xs bg-success-500/20 text-success-400 rounded">
{t('admin.tariffs.trial')}
</span>
)}
{!tariff.is_active && (
<span className="px-2 py-0.5 text-xs bg-dark-600 text-dark-400 rounded">
{t('admin.tariffs.inactive')}
</span>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
{tariff.is_daily && tariff.daily_price_kopeks > 0 && (
<span className="text-amber-400">{(tariff.daily_price_kopeks / 100).toFixed(2)} /день</span>
)}
<span>
{tariff.traffic_limit_gb === 0
? t('admin.tariffs.unlimited')
: `${tariff.traffic_limit_gb} GB`
}
</span>
<span>{tariff.device_limit} {t('admin.tariffs.devices')}</span>
<span>{tariff.servers_count} {t('admin.tariffs.servers')}</span>
<span>{tariff.subscriptions_count} {t('admin.tariffs.subscriptions')}</span>
</div>
</div>
<div className="flex items-center gap-2">
{/* Toggle Active */}
<button
onClick={() => toggleMutation.mutate(tariff.id)}
className={`p-2 rounded-lg transition-colors ${
tariff.is_active
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
}`}
title={tariff.is_active ? t('admin.tariffs.deactivate') : t('admin.tariffs.activate')}
>
{tariff.is_active ? <CheckIcon /> : <XIcon />}
</button>
{/* Toggle Trial */}
<button
onClick={() => toggleTrialMutation.mutate(tariff.id)}
className={`p-2 rounded-lg transition-colors ${
tariff.is_trial_available
? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30'
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
}`}
title={t('admin.tariffs.toggleTrial')}
>
T
</button>
{/* Edit */}
<button
onClick={() => handleEdit(tariff.id)}
className="p-2 bg-dark-700 text-dark-300 rounded-lg hover:bg-dark-600 hover:text-dark-100 transition-colors"
title={t('admin.tariffs.edit')}
>
<EditIcon />
</button>
{/* Delete */}
<button
onClick={() => setDeleteConfirm(tariff.id)}
className="p-2 bg-dark-700 text-dark-300 rounded-lg hover:bg-error-500/20 hover:text-error-400 transition-colors"
title={t('admin.tariffs.delete')}
disabled={tariff.subscriptions_count > 0}
>
<TrashIcon />
</button>
</div>
</div>
</div>
))}
</div>
)}
{/* Type Selection Modal */}
{showTypeSelect && (
<TariffTypeSelect
onSelect={handleTypeSelect}
onClose={() => setShowTypeSelect(false)}
/>
)}
{/* Period Tariff Modal */}
{showPeriodModal && (
<PeriodTariffModal
tariff={editingTariff}
servers={servers}
onSave={handleSave}
onClose={handleCloseModal}
isLoading={createMutation.isPending || updateMutation.isPending}
/>
)}
{/* Daily Tariff Modal */}
{showDailyModal && (
<DailyTariffModal
tariff={editingTariff}
servers={servers}
onSave={handleSave}
onClose={handleCloseModal}
isLoading={createMutation.isPending || updateMutation.isPending}
/>
)}
{/* Delete Confirmation */}
{deleteConfirm !== null && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<div className="bg-dark-800 rounded-xl p-6 max-w-sm w-full">
<h3 className="text-lg font-semibold text-dark-100 mb-2">{t('admin.tariffs.confirmDelete')}</h3>
<p className="text-dark-400 mb-6">{t('admin.tariffs.confirmDeleteText')}</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setDeleteConfirm(null)}
className="px-4 py-2 text-dark-300 hover:text-dark-100 transition-colors"
>
{t('common.cancel')}
</button>
<button
onClick={() => deleteMutation.mutate(deleteConfirm)}
className="px-4 py-2 bg-error-500 text-white rounded-lg hover:bg-error-600 transition-colors"
>
{t('common.delete')}
</button>
</div>
</div>
</div>
)}
</div>
)
}