Merge branch 'main' from bedolaga

This commit is contained in:
PEDZEO
2026-01-18 07:02:14 +03:00
39 changed files with 5027 additions and 592 deletions

39
src/api/adminPayments.ts Normal file
View File

@@ -0,0 +1,39 @@
import apiClient from './client'
import type { PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
export interface PaymentsStats {
total_pending: number
by_method: Record<string, number>
}
export const adminPaymentsApi = {
// Get all pending payments (admin)
getPendingPayments: async (params?: {
page?: number
per_page?: number
method_filter?: string
}): Promise<PaginatedResponse<PendingPayment>> => {
const response = await apiClient.get<PaginatedResponse<PendingPayment>>('/cabinet/admin/payments', {
params,
})
return response.data
},
// Get payments statistics
getStats: async (): Promise<PaymentsStats> => {
const response = await apiClient.get<PaymentsStats>('/cabinet/admin/payments/stats')
return response.data
},
// Get specific payment details
getPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(`/cabinet/admin/payments/${method}/${paymentId}`)
return response.data
},
// Manually check payment status
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
const response = await apiClient.post<ManualCheckResponse>(`/cabinet/admin/payments/${method}/${paymentId}/check`)
return response.data
},
}

386
src/api/adminRemnawave.ts Normal file
View File

@@ -0,0 +1,386 @@
import { apiClient } from './client'
// ============ Types ============
// Status & Connection
export interface ConnectionStatus {
status: string
message: string
api_url?: string
status_code?: number
system_info?: Record<string, unknown>
}
export interface RemnaWaveStatusResponse {
is_configured: boolean
configuration_error?: string
connection?: ConnectionStatus
}
// System Statistics
export interface SystemSummary {
users_online: number
total_users: number
active_connections: number
nodes_online: number
users_last_day: number
users_last_week: number
users_never_online: number
total_user_traffic: number
}
export interface ServerInfo {
cpu_cores: number
cpu_physical_cores: number
memory_total: number
memory_used: number
memory_free: number
memory_available: number
uptime_seconds: number
}
export interface Bandwidth {
realtime_download: number
realtime_upload: number
realtime_total: number
}
export interface TrafficPeriod {
current: number
previous: number
difference?: string
}
export interface TrafficPeriods {
last_2_days: TrafficPeriod
last_7_days: TrafficPeriod
last_30_days: TrafficPeriod
current_month: TrafficPeriod
current_year: TrafficPeriod
}
export interface SystemStatsResponse {
system: SystemSummary
users_by_status: Record<string, number>
server_info: ServerInfo
bandwidth: Bandwidth
traffic_periods: TrafficPeriods
nodes_realtime: Record<string, unknown>[]
nodes_weekly: Record<string, unknown>[]
last_updated?: string
}
// Nodes
export interface NodeInfo {
uuid: string
name: string
address: string
country_code?: string
is_connected: boolean
is_disabled: boolean
is_node_online: boolean
is_xray_running: boolean
users_online?: number
traffic_used_bytes?: number
traffic_limit_bytes?: number
last_status_change?: string
last_status_message?: string
xray_uptime?: string
is_traffic_tracking_active: boolean
traffic_reset_day?: number
notify_percent?: number
consumption_multiplier: number
cpu_count?: number
cpu_model?: string
total_ram?: string
created_at?: string
updated_at?: string
provider_uuid?: string
}
export interface NodesListResponse {
items: NodeInfo[]
total: number
}
export interface NodesOverview {
total: number
online: number
offline: number
disabled: number
total_users_online: number
nodes: NodeInfo[]
}
export interface NodeStatisticsResponse {
node: NodeInfo
realtime?: Record<string, unknown>
usage_history: Record<string, unknown>[]
last_updated?: string
}
export interface NodeActionResponse {
success: boolean
message?: string
is_disabled?: boolean
}
// Squads
export interface SquadWithLocalInfo {
uuid: string
name: string
members_count: number
inbounds_count: number
inbounds: Record<string, unknown>[]
local_id?: number
display_name?: string
country_code?: string
is_available?: boolean
is_trial_eligible?: boolean
price_kopeks?: number
max_users?: number
current_users?: number
is_synced: boolean
}
export interface SquadsListResponse {
items: SquadWithLocalInfo[]
total: number
}
export interface SquadDetailResponse extends SquadWithLocalInfo {
description?: string
sort_order?: number
active_subscriptions: number
}
export interface SquadOperationResponse {
success: boolean
message?: string
data?: Record<string, unknown>
}
// Migration
export interface MigrationPreviewResponse {
squad_uuid: string
squad_name: string
current_users: number
max_users?: number
users_to_migrate: number
}
export interface MigrationStats {
source_uuid: string
target_uuid: string
total: number
updated: number
panel_updated: number
panel_failed: number
source_removed: number
target_added: number
}
export interface MigrationResponse {
success: boolean
message?: string
error?: string
data?: MigrationStats
}
// Inbounds
export interface InboundsListResponse {
items: Record<string, unknown>[]
total: number
}
// Auto Sync
export interface AutoSyncStatus {
enabled: boolean
times: string[]
next_run?: string
is_running: boolean
last_run_started_at?: string
last_run_finished_at?: string
last_run_success?: boolean
last_run_reason?: string
last_run_error?: string
last_user_stats?: Record<string, unknown>
last_server_stats?: Record<string, unknown>
}
export interface AutoSyncRunResponse {
started: boolean
success?: boolean
error?: string
user_stats?: Record<string, unknown>
server_stats?: Record<string, unknown>
reason?: string
}
// Sync
export interface SyncResponse {
success: boolean
message?: string
data?: Record<string, unknown>
}
// ============ API ============
export const adminRemnawaveApi = {
// Status & Connection
getStatus: async (): Promise<RemnaWaveStatusResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/status')
return response.data
},
// System Statistics
getSystemStats: async (): Promise<SystemStatsResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/system')
return response.data
},
// Nodes
getNodes: async (): Promise<NodesListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes')
return response.data
},
getNodesOverview: async (): Promise<NodesOverview> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/overview')
return response.data
},
getNodesRealtime: async (): Promise<Record<string, unknown>[]> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/realtime')
return response.data
},
getNode: async (uuid: string): Promise<NodeInfo> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}`)
return response.data
},
getNodeStatistics: async (uuid: string): Promise<NodeStatisticsResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}/statistics`)
return response.data
},
nodeAction: async (uuid: string, action: 'enable' | 'disable' | 'restart'): Promise<NodeActionResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/nodes/${uuid}/action`, { action })
return response.data
},
restartAllNodes: async (): Promise<NodeActionResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/nodes/restart-all')
return response.data
},
// Squads
getSquads: async (): Promise<SquadsListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/squads')
return response.data
},
getSquad: async (uuid: string): Promise<SquadDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/squads/${uuid}`)
return response.data
},
createSquad: async (data: { name: string; inbound_uuids?: string[] }): Promise<SquadOperationResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/squads', data)
return response.data
},
updateSquad: async (uuid: string, data: { name?: string; inbound_uuids?: string[] }): Promise<SquadOperationResponse> => {
const response = await apiClient.patch(`/cabinet/admin/remnawave/squads/${uuid}`, data)
return response.data
},
deleteSquad: async (uuid: string): Promise<SquadOperationResponse> => {
const response = await apiClient.delete(`/cabinet/admin/remnawave/squads/${uuid}`)
return response.data
},
squadAction: async (uuid: string, data: {
action: 'add_all_users' | 'remove_all_users' | 'delete' | 'rename' | 'update_inbounds'
name?: string
inbound_uuids?: string[]
}): Promise<SquadOperationResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/squads/${uuid}/action`, data)
return response.data
},
// Migration
getMigrationPreview: async (uuid: string): Promise<MigrationPreviewResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/squads/${uuid}/migration-preview`)
return response.data
},
migrateSquad: async (sourceUuid: string, targetUuid: string): Promise<MigrationResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/squads/migrate', {
source_uuid: sourceUuid,
target_uuid: targetUuid,
})
return response.data
},
// Inbounds
getInbounds: async (): Promise<InboundsListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/inbounds')
return response.data
},
// Auto Sync
getAutoSyncStatus: async (): Promise<AutoSyncStatus> => {
const response = await apiClient.get('/cabinet/admin/remnawave/sync/auto/status')
return response.data
},
toggleAutoSync: async (enabled: boolean): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/toggle', { enabled })
return response.data
},
runAutoSync: async (): Promise<AutoSyncRunResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/run')
return response.data
},
// Manual Sync
syncFromPanel: async (mode: 'all' | 'new_only' | 'update_only' = 'all'): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/from-panel', { mode })
return response.data
},
syncToPanel: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/to-panel')
return response.data
},
syncServers: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/servers')
return response.data
},
validateSubscriptions: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/validate')
return response.data
},
cleanupSubscriptions: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/cleanup')
return response.data
},
syncSubscriptionStatuses: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/statuses')
return response.data
},
getSyncRecommendations: async (): Promise<SyncResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/sync/recommendations')
return response.data
},
}
export default adminRemnawaveApi

View File

@@ -1,5 +1,5 @@
import apiClient from './client'
import type { Balance, Transaction, PaymentMethod, PaginatedResponse } from '../types'
import apiClient from './client'
import type { Balance, Transaction, PaymentMethod, PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
export const balanceApi = {
// Get current balance
@@ -73,5 +73,28 @@ export const balanceApi = {
})
return response.data
},
// Get pending payments for manual verification
getPendingPayments: async (params?: {
page?: number
per_page?: number
}): Promise<PaginatedResponse<PendingPayment>> => {
const response = await apiClient.get<PaginatedResponse<PendingPayment>>('/cabinet/balance/pending-payments', {
params,
})
return response.data
},
// Get specific pending payment details
getPendingPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(`/cabinet/balance/pending-payments/${method}/${paymentId}`)
return response.data
},
// Manually check payment status
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
const response = await apiClient.post<ManualCheckResponse>(`/cabinet/balance/pending-payments/${method}/${paymentId}/check`)
return response.data
},
}

View File

@@ -1,27 +1,47 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { tokenStorage, isTokenExpired, tokenRefreshManager, safeRedirectToLogin } from '../utils/token'
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'
const TELEGRAM_INIT_STORAGE_KEY = 'telegram_init_data'
// Настраиваем endpoint для refresh
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`)
// CSRF token management
const CSRF_COOKIE_NAME = 'csrf_token'
const CSRF_HEADER_NAME = 'X-CSRF-Token'
function getCsrfToken(): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(new RegExp(`(^| )${CSRF_COOKIE_NAME}=([^;]+)`))
return match ? match[2] : null
}
function generateCsrfToken(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
}
function ensureCsrfToken(): string {
let token = getCsrfToken()
if (!token) {
token = generateCsrfToken()
// Set cookie with SameSite=Strict for CSRF protection
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`
}
return token
}
const getTelegramInitData = (): string | null => {
if (typeof window === 'undefined') return null
const initData = window.Telegram?.WebApp?.initData
if (initData) {
try {
localStorage.setItem(TELEGRAM_INIT_STORAGE_KEY, initData)
} catch {
/* ignore storage errors */
}
tokenStorage.setTelegramInitData(initData)
return initData
}
try {
return localStorage.getItem(TELEGRAM_INIT_STORAGE_KEY)
} catch {
return null
}
return tokenStorage.getTelegramInitData()
}
export const apiClient = axios.create({
@@ -31,9 +51,24 @@ export const apiClient = axios.create({
},
})
// Request interceptor - add auth token
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('access_token')
// Request interceptor - add auth token with expiration check
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
let token = tokenStorage.getAccessToken()
// Проверяем срок действия токена перед запросом
if (token && isTokenExpired(token)) {
// Используем централизованный менеджер для refresh
const newToken = await tokenRefreshManager.refreshAccessToken()
if (newToken) {
token = newToken
} else {
// Refresh не удался - редирект на логин
tokenStorage.clearTokens()
safeRedirectToLogin()
return config
}
}
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
@@ -42,41 +77,36 @@ apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (telegramInitData && config.headers) {
config.headers['X-Telegram-Init-Data'] = telegramInitData
}
// Add CSRF token for state-changing methods
const method = config.method?.toUpperCase()
if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) {
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken()
}
return config
})
// Response interceptor - handle 401, refresh token
// Response interceptor - handle 401 as fallback
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
// Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала)
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const refreshToken = localStorage.getItem('refresh_token')
if (refreshToken) {
try {
const response = await axios.post(`${API_BASE_URL}/cabinet/auth/refresh`, {
refresh_token: refreshToken,
})
const { access_token } = response.data
localStorage.setItem('access_token', access_token)
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${access_token}`
}
return apiClient(originalRequest)
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user')
window.location.href = '/login'
return Promise.reject(refreshError)
const newToken = await tokenRefreshManager.refreshAccessToken()
if (newToken) {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`
}
return apiClient(originalRequest)
} else {
// Refresh не удался
tokenStorage.clearTokens()
safeRedirectToLogin()
}
}
@@ -85,4 +115,3 @@ apiClient.interceptors.response.use(
)
export default apiClient

View File

@@ -1,6 +1,8 @@
// Currency exchange rate API
// Uses free exchangerate.host API
import axios from 'axios'
interface ExchangeRates {
USD: number
CNY: number
@@ -33,39 +35,38 @@ export const currencyApi = {
try {
// Try primary API (exchangerate.host) - get RUB based rates
const response = await fetch(
'https://api.exchangerate.host/latest?base=RUB&symbols=USD,CNY,IRR'
)
const response = await axios.get<{
success?: boolean
rates?: { USD?: number; CNY?: number; IRR?: number }
}>('https://api.exchangerate.host/latest', {
params: { base: 'RUB', symbols: 'USD,CNY,IRR' },
})
if (response.ok) {
const data = await response.json()
if (data.success && data.rates) {
// API returns how much of each currency equals 1 RUB
// We need the inverse: how many RUB equals 1 of each currency
const rates: ExchangeRates = {
USD: data.rates.USD ? 1 / data.rates.USD : FALLBACK_RATES.USD,
CNY: data.rates.CNY ? 1 / data.rates.CNY : FALLBACK_RATES.CNY,
IRR: data.rates.IRR ? 1 / data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
if (response.data.success && response.data.rates) {
// API returns how much of each currency equals 1 RUB
// We need the inverse: how many RUB equals 1 of each currency
const rates: ExchangeRates = {
USD: response.data.rates.USD ? 1 / response.data.rates.USD : FALLBACK_RATES.USD,
CNY: response.data.rates.CNY ? 1 / response.data.rates.CNY : FALLBACK_RATES.CNY,
IRR: response.data.rates.IRR ? 1 / response.data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
}
// Try backup API (open.er-api.com)
const backupResponse = await fetch('https://open.er-api.com/v6/latest/RUB')
const backupResponse = await axios.get<{
rates?: { USD?: number; CNY?: number; IRR?: number }
}>('https://open.er-api.com/v6/latest/RUB')
if (backupResponse.ok) {
const backupData = await backupResponse.json()
if (backupData.rates) {
const rates: ExchangeRates = {
USD: backupData.rates.USD ? 1 / backupData.rates.USD : FALLBACK_RATES.USD,
CNY: backupData.rates.CNY ? 1 / backupData.rates.CNY : FALLBACK_RATES.CNY,
IRR: backupData.rates.IRR ? 1 / backupData.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
if (backupResponse.data.rates) {
const rates: ExchangeRates = {
USD: backupResponse.data.rates.USD ? 1 / backupResponse.data.rates.USD : FALLBACK_RATES.USD,
CNY: backupResponse.data.rates.CNY ? 1 / backupResponse.data.rates.CNY : FALLBACK_RATES.CNY,
IRR: backupResponse.data.rates.IRR ? 1 / backupResponse.data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
}
// Return fallback rates if both APIs fail

View File

@@ -1,4 +1,6 @@
export interface MiniappCreatePaymentPayload {
import axios, { AxiosError } from 'axios'
export interface MiniappCreatePaymentPayload {
initData: string
method: string
amountKopeks?: number | null
@@ -16,21 +18,26 @@ export const miniappApi = {
createPayment: async (
payload: MiniappCreatePaymentPayload
): Promise<MiniappCreatePaymentResponse> => {
const res = await fetch('/miniapp/payments/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initData: payload.initData || '',
method: payload.method,
amountKopeks: payload.amountKopeks ?? null,
option: payload.option ?? null,
}),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
const message = (data && (data.detail || data.message)) || 'Failed to create payment'
throw new Error(String(message))
try {
const response = await axios.post<MiniappCreatePaymentResponse>(
'/miniapp/payments/create',
{
initData: payload.initData || '',
method: payload.method,
amountKopeks: payload.amountKopeks ?? null,
option: payload.option ?? null,
},
{
headers: { 'Content-Type': 'application/json' },
}
)
return response.data
} catch (error) {
const axiosError = error as AxiosError<{ detail?: string; message?: string }>
const message = axiosError.response?.data?.detail
|| axiosError.response?.data?.message
|| 'Failed to create payment'
throw new Error(message)
}
return data as MiniappCreatePaymentResponse
},
}

227
src/api/promoOffers.ts Normal file
View File

@@ -0,0 +1,227 @@
import apiClient from './client'
// ============== Types ==============
export interface PromoOfferUserInfo {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string | null
}
export interface PromoOfferSubscriptionInfo {
id: number
status: string
is_trial: boolean
start_date: string
end_date: string
autopay_enabled: boolean
}
export interface PromoOffer {
id: number
user_id: number
subscription_id: number | null
notification_type: string
discount_percent: number
bonus_amount_kopeks: number
expires_at: string
claimed_at: string | null
is_active: boolean
effect_type: string
extra_data: Record<string, any>
created_at: string
updated_at: string
user: PromoOfferUserInfo | null
subscription: PromoOfferSubscriptionInfo | null
}
export interface PromoOfferListResponse {
items: PromoOffer[]
total: number
limit: number
offset: number
}
export interface PromoOfferBroadcastRequest {
notification_type: string
valid_hours: number
discount_percent?: number
bonus_amount_kopeks?: number
effect_type?: string
extra_data?: Record<string, any>
target?: string
user_id?: number
telegram_id?: number
}
export interface PromoOfferBroadcastResponse {
created_offers: number
user_ids: number[]
target: string | null
}
export interface PromoOfferTemplate {
id: number
name: string
offer_type: string
message_text: string
button_text: string
valid_hours: number
discount_percent: number
bonus_amount_kopeks: number
active_discount_hours: number | null
test_duration_hours: number | null
test_squad_uuids: string[]
is_active: boolean
created_by: number | null
created_at: string
updated_at: string
}
export interface PromoOfferTemplateListResponse {
items: PromoOfferTemplate[]
}
export interface PromoOfferTemplateUpdateRequest {
name?: string
message_text?: string
button_text?: string
valid_hours?: number
discount_percent?: number
bonus_amount_kopeks?: number
active_discount_hours?: number
test_duration_hours?: number
test_squad_uuids?: string[]
is_active?: boolean
}
export interface PromoOfferLogOfferInfo {
id: number
notification_type: string | null
discount_percent: number | null
bonus_amount_kopeks: number | null
effect_type: string | null
expires_at: string | null
claimed_at: string | null
is_active: boolean | null
}
export interface PromoOfferLog {
id: number
user_id: number | null
offer_id: number | null
action: string
source: string | null
percent: number | null
effect_type: string | null
details: Record<string, any>
created_at: string
user: PromoOfferUserInfo | null
offer: PromoOfferLogOfferInfo | null
}
export interface PromoOfferLogListResponse {
items: PromoOfferLog[]
total: number
limit: number
offset: number
}
// Target segments for broadcast
export const TARGET_SEGMENTS = {
all: 'Все пользователи',
active: 'Активные подписчики',
trial: 'Триал пользователи',
trial_ending: 'Заканчивается триал',
expiring: 'Заканчивается подписка',
expired: 'Истекшая подписка',
zero: 'Нулевой баланс',
autopay_failed: 'Ошибка автоплатежа',
low_balance: 'Низкий баланс',
inactive_30d: 'Неактивны 30 дней',
inactive_60d: 'Неактивны 60 дней',
inactive_90d: 'Неактивны 90 дней',
custom_today: 'Зарегистрированы сегодня',
custom_week: 'Зарегистрированы за неделю',
custom_month: 'Зарегистрированы за месяц',
custom_active_today: 'Активны сегодня',
} as const
export type TargetSegment = keyof typeof TARGET_SEGMENTS
// Offer type configurations
export const OFFER_TYPE_CONFIG = {
test_access: {
icon: '🧪',
label: 'Тестовый доступ',
effect: 'test_access',
description: 'Временный доступ к дополнительным серверам',
},
extend_discount: {
icon: '💎',
label: 'Скидка на продление',
effect: 'percent_discount',
description: 'Скидка для текущих подписчиков',
},
purchase_discount: {
icon: '🎯',
label: 'Скидка на покупку',
effect: 'percent_discount',
description: 'Скидка для новых пользователей',
},
} as const
export type OfferType = keyof typeof OFFER_TYPE_CONFIG
// ============== API ==============
export const promoOffersApi = {
// Get list of promo offers
getOffers: async (params?: {
limit?: number
offset?: number
user_id?: number
is_active?: boolean
}): Promise<PromoOfferListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers', { params })
return response.data
},
// Broadcast offer to multiple users
broadcastOffer: async (data: PromoOfferBroadcastRequest): Promise<PromoOfferBroadcastResponse> => {
const response = await apiClient.post('/cabinet/admin/promo-offers/broadcast', data)
return response.data
},
// Get promo offer logs
getLogs: async (params?: {
limit?: number
offset?: number
user_id?: number
action?: string
}): Promise<PromoOfferLogListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers/logs', { params })
return response.data
},
// Get all templates
getTemplates: async (): Promise<PromoOfferTemplateListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers/templates')
return response.data
},
// Get single template
getTemplate: async (id: number): Promise<PromoOfferTemplate> => {
const response = await apiClient.get(`/cabinet/admin/promo-offers/templates/${id}`)
return response.data
},
// Update template
updateTemplate: async (id: number, data: PromoOfferTemplateUpdateRequest): Promise<PromoOfferTemplate> => {
const response = await apiClient.patch(`/cabinet/admin/promo-offers/templates/${id}`, data)
return response.data
},
}