From dc1769520612286bc2da4bd25c23a60ab38792f3 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Mon, 2 Feb 2026 01:38:01 +0300 Subject: [PATCH] feat: replace tariff creation modal with dedicated page --- src/App.tsx | 21 + src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/pages/AdminTariffCreate.tsx | 972 +++++++++++++++++++++ src/pages/AdminTariffs.tsx | 1413 +------------------------------ 7 files changed, 1007 insertions(+), 1403 deletions(-) create mode 100644 src/pages/AdminTariffCreate.tsx diff --git a/src/App.tsx b/src/App.tsx index 60665f0..ac99b35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ const AdminSettings = lazy(() => import('./pages/AdminSettings')); const AdminApps = lazy(() => import('./pages/AdminApps')); const AdminWheel = lazy(() => import('./pages/AdminWheel')); const AdminTariffs = lazy(() => import('./pages/AdminTariffs')); +const AdminTariffCreate = lazy(() => import('./pages/AdminTariffCreate')); const AdminServers = lazy(() => import('./pages/AdminServers')); const AdminDashboard = lazy(() => import('./pages/AdminDashboard')); const AdminBanSystem = lazy(() => import('./pages/AdminBanSystem')); @@ -289,6 +290,26 @@ function App() { } /> + + + + + + } + /> + + + + + + } + /> ( + + + +); + +const TrashIcon = () => ( + + + +); + +const CheckIcon = () => ( + + + +); + +const InfinityIcon = () => ( + + + +); + +const CalendarIcon = () => ( + + + +); + +const SunIcon = () => ( + + + +); + +type TariffType = 'period' | 'daily' | null; + +export default function AdminTariffCreate() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); + const isEdit = !!id; + + // Step: null = type selection, 'period' or 'daily' = form + const [tariffType, setTariffType] = useState(null); + + // Form state + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [trafficLimitGb, setTrafficLimitGb] = useState(100); + const [deviceLimit, setDeviceLimit] = useState(1); + const [devicePriceKopeks, setDevicePriceKopeks] = useState(0); + const [maxDeviceLimit, setMaxDeviceLimit] = useState(0); + const [tierLevel, setTierLevel] = useState(1); + const [periodPrices, setPeriodPrices] = useState([]); + const [selectedSquads, setSelectedSquads] = useState([]); + const [dailyPriceKopeks, setDailyPriceKopeks] = useState(0); + + // Traffic topup + const [trafficTopupEnabled, setTrafficTopupEnabled] = useState(false); + const [maxTopupTrafficGb, setMaxTopupTrafficGb] = useState(0); + const [trafficTopupPackages, setTrafficTopupPackages] = useState>({}); + + // Traffic reset mode + const [trafficResetMode, setTrafficResetMode] = useState(null); + + // Custom days (period tariff) + const [customDaysEnabled, setCustomDaysEnabled] = useState(false); + const [pricePerDayKopeks, setPricePerDayKopeks] = useState(0); + const [minDays, setMinDays] = useState(1); + const [maxDays, setMaxDays] = useState(365); + + // Custom traffic (period tariff) + const [customTrafficEnabled, setCustomTrafficEnabled] = useState(false); + const [trafficPricePerGbKopeks, setTrafficPricePerGbKopeks] = useState(0); + const [minTrafficGb, setMinTrafficGb] = useState(1); + const [maxTrafficGb, setMaxTrafficGb] = useState(1000); + + // New period for adding + const [newPeriodDays, setNewPeriodDays] = useState(30); + const [newPeriodPrice, setNewPeriodPrice] = useState(300); + + const [activeTab, setActiveTab] = useState<'basic' | 'periods' | 'servers' | 'extra'>('basic'); + + // Fetch servers + const { data: servers = [] } = useQuery({ + queryKey: ['admin-tariffs-servers'], + queryFn: () => tariffsApi.getAvailableServers(), + }); + + // Fetch tariff for editing + const { isLoading: isLoadingTariff } = useQuery({ + queryKey: ['admin-tariff', id], + queryFn: () => tariffsApi.getTariff(Number(id)), + enabled: isEdit, + staleTime: 0, + gcTime: 0, + refetchOnMount: true, + refetchOnWindowFocus: false, + select: useCallback((data: TariffDetail) => { + // Set form values from tariff + setTariffType(data.is_daily ? 'daily' : 'period'); + setName(data.name); + setDescription(data.description || ''); + setTrafficLimitGb(data.traffic_limit_gb ?? 100); + setDeviceLimit(data.device_limit || 1); + setDevicePriceKopeks(data.device_price_kopeks || 0); + setMaxDeviceLimit(data.max_device_limit || 0); + setTierLevel(data.tier_level || 1); + setPeriodPrices(data.period_prices?.length ? data.period_prices : []); + setSelectedSquads(data.allowed_squads || []); + setDailyPriceKopeks(data.daily_price_kopeks || 0); + setTrafficTopupEnabled(data.traffic_topup_enabled || false); + setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0); + setTrafficTopupPackages(data.traffic_topup_packages || {}); + setTrafficResetMode(data.traffic_reset_mode || null); + setCustomDaysEnabled(data.custom_days_enabled || false); + setPricePerDayKopeks(data.price_per_day_kopeks || 0); + setMinDays(data.min_days || 1); + setMaxDays(data.max_days || 365); + setCustomTrafficEnabled(data.custom_traffic_enabled || false); + setTrafficPricePerGbKopeks(data.traffic_price_per_gb_kopeks || 0); + setMinTrafficGb(data.min_traffic_gb || 1); + setMaxTrafficGb(data.max_traffic_gb || 1000); + return data; + }, []), + }); + + // Create mutation + const createMutation = useMutation({ + mutationFn: tariffsApi.createTariff, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] }); + navigate('/admin/tariffs'); + }, + }); + + // Update mutation + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: number; data: TariffUpdateRequest }) => + tariffsApi.updateTariff(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] }); + navigate('/admin/tariffs'); + }, + }); + + const handleSubmit = () => { + const isDaily = tariffType === 'daily'; + + 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: isDaily ? [] : 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: isDaily, + daily_price_kopeks: isDaily ? dailyPriceKopeks : 0, + traffic_reset_mode: trafficResetMode, + }; + + // Add period tariff specific fields + if (!isDaily) { + data.custom_days_enabled = customDaysEnabled; + data.price_per_day_kopeks = pricePerDayKopeks; + data.min_days = minDays; + data.max_days = maxDays; + data.custom_traffic_enabled = customTrafficEnabled; + data.traffic_price_per_gb_kopeks = trafficPricePerGbKopeks; + data.min_traffic_gb = minTrafficGb; + data.max_traffic_gb = maxTrafficGb; + } + + if (isEdit) { + updateMutation.mutate({ id: Number(id), data }); + } else { + createMutation.mutate(data as TariffCreateRequest); + } + }; + + 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)), + ); + }; + + const isLoading = createMutation.isPending || updateMutation.isPending; + + const isValidPeriod = name && (periodPrices.length > 0 || customDaysEnabled); + const isValidDaily = name && dailyPriceKopeks > 0; + const isValid = + tariffType === 'period' ? isValidPeriod : tariffType === 'daily' ? isValidDaily : false; + + // Loading state + if (isEdit && isLoadingTariff) { + return ( +
+
+
+ ); + } + + // Type selection step (only for creation) + if (!isEdit && tariffType === null) { + return ( +
+
+ +
+

{t('admin.tariffs.selectType')}

+

{t('admin.tariffs.selectTypeDesc')}

+
+
+ +
+ + +
+
+ ); + } + + const isDaily = tariffType === 'daily'; + const accentColor = isDaily ? 'warning' : 'accent'; + + return ( +
+ {/* Header */} +
+ +
+
+ {isDaily ? : } +
+
+

+ {isEdit + ? t('admin.tariffs.editTitle') + : isDaily + ? t('admin.tariffs.newDailyTitle') + : t('admin.tariffs.newPeriodTitle')} +

+

+ {isDaily ? t('admin.tariffs.dailyDeduction') : t('admin.tariffs.periodPayment')} +

+
+
+
+ + {/* Tabs */} +
+ {(isDaily + ? (['basic', 'servers', 'extra'] as const) + : (['basic', 'periods', 'servers', 'extra'] as const) + ).map((tab) => ( + + ))} +
+ + {/* Content */} +
+ {activeTab === 'basic' && ( +
+ {/* Name */} +
+ + setName(e.target.value)} + className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" + placeholder={ + isDaily + ? t('admin.tariffs.nameExampleDaily') + : t('admin.tariffs.nameExamplePeriod') + } + /> +
+ + {/* Description */} +
+ +