mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
refactor: migrate to eslint flat config and format codebase with prettier
- Remove legacy .eslintrc.cjs and .eslintignore - Add eslint.config.js with flat config, security rules (no-eval, no-implied-eval, no-new-func, no-script-url) - Add .prettierrc and .prettierignore - Format entire codebase with prettier
This commit is contained in:
434
src/api/admin.ts
434
src/api/admin.ts
@@ -1,306 +1,310 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface AdminTicketUser {
|
||||
id: number
|
||||
telegram_id: number
|
||||
username: string | null
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
id: number;
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
}
|
||||
|
||||
export interface AdminTicketMessage {
|
||||
id: number
|
||||
message_text: string
|
||||
is_from_admin: boolean
|
||||
has_media: boolean
|
||||
media_type: string | null
|
||||
media_file_id: string | null
|
||||
media_caption: string | null
|
||||
created_at: string
|
||||
id: number;
|
||||
message_text: string;
|
||||
is_from_admin: boolean;
|
||||
has_media: boolean;
|
||||
media_type: string | null;
|
||||
media_file_id: string | null;
|
||||
media_caption: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AdminTicket {
|
||||
id: number
|
||||
title: string
|
||||
status: string
|
||||
priority: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
closed_at: string | null
|
||||
messages_count: number
|
||||
user: AdminTicketUser | null
|
||||
last_message: AdminTicketMessage | null
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
closed_at: string | null;
|
||||
messages_count: number;
|
||||
user: AdminTicketUser | null;
|
||||
last_message: AdminTicketMessage | null;
|
||||
}
|
||||
|
||||
export interface AdminTicketDetail {
|
||||
id: number
|
||||
title: string
|
||||
status: string
|
||||
priority: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
closed_at: string | null
|
||||
is_reply_blocked: boolean
|
||||
user: AdminTicketUser | null
|
||||
messages: AdminTicketMessage[]
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
closed_at: string | null;
|
||||
is_reply_blocked: boolean;
|
||||
user: AdminTicketUser | null;
|
||||
messages: AdminTicketMessage[];
|
||||
}
|
||||
|
||||
export interface AdminTicketStats {
|
||||
total: number
|
||||
open: number
|
||||
pending: number
|
||||
answered: number
|
||||
closed: number
|
||||
total: number;
|
||||
open: number;
|
||||
pending: number;
|
||||
answered: number;
|
||||
closed: number;
|
||||
}
|
||||
|
||||
export interface TicketSettings {
|
||||
sla_enabled: boolean
|
||||
sla_minutes: number
|
||||
sla_check_interval_seconds: number
|
||||
sla_reminder_cooldown_minutes: number
|
||||
support_system_mode: string // tickets, contact, both
|
||||
cabinet_user_notifications_enabled: boolean
|
||||
cabinet_admin_notifications_enabled: boolean
|
||||
sla_enabled: boolean;
|
||||
sla_minutes: number;
|
||||
sla_check_interval_seconds: number;
|
||||
sla_reminder_cooldown_minutes: number;
|
||||
support_system_mode: string; // tickets, contact, both
|
||||
cabinet_user_notifications_enabled: boolean;
|
||||
cabinet_admin_notifications_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface TicketSettingsUpdate {
|
||||
sla_enabled?: boolean
|
||||
sla_minutes?: number
|
||||
sla_check_interval_seconds?: number
|
||||
sla_reminder_cooldown_minutes?: number
|
||||
support_system_mode?: string
|
||||
cabinet_user_notifications_enabled?: boolean
|
||||
cabinet_admin_notifications_enabled?: boolean
|
||||
sla_enabled?: boolean;
|
||||
sla_minutes?: number;
|
||||
sla_check_interval_seconds?: number;
|
||||
sla_reminder_cooldown_minutes?: number;
|
||||
support_system_mode?: string;
|
||||
cabinet_user_notifications_enabled?: boolean;
|
||||
cabinet_admin_notifications_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AdminTicketListResponse {
|
||||
items: AdminTicket[]
|
||||
total: number
|
||||
page: number
|
||||
per_page: number
|
||||
pages: number
|
||||
items: AdminTicket[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
// Check if current user is admin
|
||||
checkIsAdmin: async (): Promise<{ is_admin: boolean }> => {
|
||||
const response = await apiClient.get('/cabinet/auth/me/is-admin')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/auth/me/is-admin');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get ticket statistics
|
||||
getTicketStats: async (): Promise<AdminTicketStats> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/stats')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all tickets
|
||||
getTickets: async (params: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
status?: string
|
||||
priority?: string
|
||||
} = {}): Promise<AdminTicketListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets', { params })
|
||||
return response.data
|
||||
getTickets: async (
|
||||
params: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
} = {},
|
||||
): Promise<AdminTicketListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single ticket with messages
|
||||
getTicket: async (ticketId: number): Promise<AdminTicketDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/tickets/${ticketId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/tickets/${ticketId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reply to ticket
|
||||
replyToTicket: async (ticketId: number, message: string): Promise<AdminTicketMessage> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/reply`, { message })
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/reply`, { message });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update ticket status
|
||||
updateTicketStatus: async (ticketId: number, status: string): Promise<AdminTicketDetail> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/status`, { status })
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/status`, { status });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update ticket priority
|
||||
updateTicketPriority: async (ticketId: number, priority: string): Promise<AdminTicketDetail> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/priority`, { priority })
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/priority`, {
|
||||
priority,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get ticket settings
|
||||
getTicketSettings: async (): Promise<TicketSettings> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/settings')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/settings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update ticket settings
|
||||
updateTicketSettings: async (settings: TicketSettingsUpdate): Promise<TicketSettings> => {
|
||||
const response = await apiClient.patch('/cabinet/admin/tickets/settings', settings)
|
||||
return response.data
|
||||
const response = await apiClient.patch('/cabinet/admin/tickets/settings', settings);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// ============ Dashboard Stats Types ============
|
||||
|
||||
export interface NodeStatus {
|
||||
uuid: string
|
||||
name: string
|
||||
address: string
|
||||
is_connected: boolean
|
||||
is_disabled: boolean
|
||||
users_online: number
|
||||
traffic_used_bytes?: number
|
||||
uptime?: string
|
||||
xray_version?: string
|
||||
node_version?: string
|
||||
last_status_message?: string
|
||||
xray_uptime?: string
|
||||
is_xray_running?: boolean
|
||||
cpu_count?: number
|
||||
cpu_model?: string
|
||||
total_ram?: string
|
||||
country_code?: string
|
||||
uuid: string;
|
||||
name: string;
|
||||
address: string;
|
||||
is_connected: boolean;
|
||||
is_disabled: boolean;
|
||||
users_online: number;
|
||||
traffic_used_bytes?: number;
|
||||
uptime?: string;
|
||||
xray_version?: string;
|
||||
node_version?: string;
|
||||
last_status_message?: string;
|
||||
xray_uptime?: string;
|
||||
is_xray_running?: boolean;
|
||||
cpu_count?: number;
|
||||
cpu_model?: string;
|
||||
total_ram?: string;
|
||||
country_code?: string;
|
||||
}
|
||||
|
||||
export interface NodesOverview {
|
||||
total: number
|
||||
online: number
|
||||
offline: number
|
||||
disabled: number
|
||||
total_users_online: number
|
||||
nodes: NodeStatus[]
|
||||
total: number;
|
||||
online: number;
|
||||
offline: number;
|
||||
disabled: number;
|
||||
total_users_online: number;
|
||||
nodes: NodeStatus[];
|
||||
}
|
||||
|
||||
export interface RevenueData {
|
||||
date: string
|
||||
amount_kopeks: number
|
||||
amount_rubles: number
|
||||
date: string;
|
||||
amount_kopeks: number;
|
||||
amount_rubles: number;
|
||||
}
|
||||
|
||||
export interface SubscriptionStats {
|
||||
total: number
|
||||
active: number
|
||||
trial: number
|
||||
paid: number
|
||||
expired: number
|
||||
purchased_today: number
|
||||
purchased_week: number
|
||||
purchased_month: number
|
||||
trial_to_paid_conversion: number
|
||||
total: number;
|
||||
active: number;
|
||||
trial: number;
|
||||
paid: number;
|
||||
expired: number;
|
||||
purchased_today: number;
|
||||
purchased_week: number;
|
||||
purchased_month: number;
|
||||
trial_to_paid_conversion: number;
|
||||
}
|
||||
|
||||
export interface FinancialStats {
|
||||
income_today_kopeks: number
|
||||
income_today_rubles: number
|
||||
income_month_kopeks: number
|
||||
income_month_rubles: number
|
||||
income_total_kopeks: number
|
||||
income_total_rubles: number
|
||||
subscription_income_kopeks: number
|
||||
subscription_income_rubles: number
|
||||
income_today_kopeks: number;
|
||||
income_today_rubles: number;
|
||||
income_month_kopeks: number;
|
||||
income_month_rubles: number;
|
||||
income_total_kopeks: number;
|
||||
income_total_rubles: number;
|
||||
subscription_income_kopeks: number;
|
||||
subscription_income_rubles: number;
|
||||
}
|
||||
|
||||
export interface ServerStats {
|
||||
total_servers: number
|
||||
available_servers: number
|
||||
servers_with_connections: number
|
||||
total_revenue_kopeks: number
|
||||
total_revenue_rubles: number
|
||||
total_servers: number;
|
||||
available_servers: number;
|
||||
servers_with_connections: number;
|
||||
total_revenue_kopeks: number;
|
||||
total_revenue_rubles: number;
|
||||
}
|
||||
|
||||
export interface TariffStatItem {
|
||||
tariff_id: number
|
||||
tariff_name: string
|
||||
active_subscriptions: number
|
||||
trial_subscriptions: number
|
||||
purchased_today: number
|
||||
purchased_week: number
|
||||
purchased_month: number
|
||||
tariff_id: number;
|
||||
tariff_name: string;
|
||||
active_subscriptions: number;
|
||||
trial_subscriptions: number;
|
||||
purchased_today: number;
|
||||
purchased_week: number;
|
||||
purchased_month: number;
|
||||
}
|
||||
|
||||
export interface TariffStats {
|
||||
tariffs: TariffStatItem[]
|
||||
total_tariff_subscriptions: number
|
||||
tariffs: TariffStatItem[];
|
||||
total_tariff_subscriptions: number;
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
nodes: NodesOverview
|
||||
subscriptions: SubscriptionStats
|
||||
financial: FinancialStats
|
||||
servers: ServerStats
|
||||
revenue_chart: RevenueData[]
|
||||
tariff_stats?: TariffStats
|
||||
nodes: NodesOverview;
|
||||
subscriptions: SubscriptionStats;
|
||||
financial: FinancialStats;
|
||||
servers: ServerStats;
|
||||
revenue_chart: RevenueData[];
|
||||
tariff_stats?: TariffStats;
|
||||
}
|
||||
|
||||
// ============ Extended Stats Types ============
|
||||
|
||||
export interface TopReferrerItem {
|
||||
user_id: number
|
||||
telegram_id: number
|
||||
username?: string
|
||||
display_name: string
|
||||
invited_count: number
|
||||
invited_today: number
|
||||
invited_week: number
|
||||
invited_month: number
|
||||
earnings_today_kopeks: number
|
||||
earnings_week_kopeks: number
|
||||
earnings_month_kopeks: number
|
||||
earnings_total_kopeks: number
|
||||
user_id: number;
|
||||
telegram_id: number;
|
||||
username?: string;
|
||||
display_name: string;
|
||||
invited_count: number;
|
||||
invited_today: number;
|
||||
invited_week: number;
|
||||
invited_month: number;
|
||||
earnings_today_kopeks: number;
|
||||
earnings_week_kopeks: number;
|
||||
earnings_month_kopeks: number;
|
||||
earnings_total_kopeks: number;
|
||||
}
|
||||
|
||||
export interface TopReferrersResponse {
|
||||
by_earnings: TopReferrerItem[]
|
||||
by_invited: TopReferrerItem[]
|
||||
total_referrers: number
|
||||
total_referrals: number
|
||||
total_earnings_kopeks: number
|
||||
by_earnings: TopReferrerItem[];
|
||||
by_invited: TopReferrerItem[];
|
||||
total_referrers: number;
|
||||
total_referrals: number;
|
||||
total_earnings_kopeks: number;
|
||||
}
|
||||
|
||||
export interface TopCampaignItem {
|
||||
id: number
|
||||
name: string
|
||||
start_parameter: string
|
||||
bonus_type: string
|
||||
is_active: boolean
|
||||
registrations: number
|
||||
conversions: number
|
||||
conversion_rate: number
|
||||
total_revenue_kopeks: number
|
||||
avg_revenue_per_user_kopeks: number
|
||||
created_at?: string
|
||||
id: number;
|
||||
name: string;
|
||||
start_parameter: string;
|
||||
bonus_type: string;
|
||||
is_active: boolean;
|
||||
registrations: number;
|
||||
conversions: number;
|
||||
conversion_rate: number;
|
||||
total_revenue_kopeks: number;
|
||||
avg_revenue_per_user_kopeks: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface TopCampaignsResponse {
|
||||
campaigns: TopCampaignItem[]
|
||||
total_campaigns: number
|
||||
total_registrations: number
|
||||
total_revenue_kopeks: number
|
||||
campaigns: TopCampaignItem[];
|
||||
total_campaigns: number;
|
||||
total_registrations: number;
|
||||
total_revenue_kopeks: number;
|
||||
}
|
||||
|
||||
export interface RecentPaymentItem {
|
||||
id: number
|
||||
user_id: number
|
||||
telegram_id: number
|
||||
username?: string
|
||||
display_name: string
|
||||
amount_kopeks: number
|
||||
amount_rubles: number
|
||||
type: string
|
||||
type_display: string
|
||||
payment_method?: string
|
||||
description?: string
|
||||
created_at: string
|
||||
is_completed: boolean
|
||||
id: number;
|
||||
user_id: number;
|
||||
telegram_id: number;
|
||||
username?: string;
|
||||
display_name: string;
|
||||
amount_kopeks: number;
|
||||
amount_rubles: number;
|
||||
type: string;
|
||||
type_display: string;
|
||||
payment_method?: string;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
is_completed: boolean;
|
||||
}
|
||||
|
||||
export interface RecentPaymentsResponse {
|
||||
payments: RecentPaymentItem[]
|
||||
total_count: number
|
||||
total_today_kopeks: number
|
||||
total_week_kopeks: number
|
||||
payments: RecentPaymentItem[];
|
||||
total_count: number;
|
||||
total_today_kopeks: number;
|
||||
total_week_kopeks: number;
|
||||
}
|
||||
|
||||
// ============ Dashboard Stats API ============
|
||||
@@ -308,43 +312,51 @@ export interface RecentPaymentsResponse {
|
||||
export const statsApi = {
|
||||
// Get complete dashboard stats
|
||||
getDashboardStats: async (): Promise<DashboardStats> => {
|
||||
const response = await apiClient.get('/cabinet/admin/stats/dashboard')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/stats/dashboard');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get nodes status
|
||||
getNodesStatus: async (): Promise<NodesOverview> => {
|
||||
const response = await apiClient.get('/cabinet/admin/stats/nodes')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/stats/nodes');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Restart a node
|
||||
restartNode: async (nodeUuid: string): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/restart`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/restart`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle node (enable/disable)
|
||||
toggleNode: async (nodeUuid: string): Promise<{ success: boolean; message: string; is_disabled: boolean }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/toggle`)
|
||||
return response.data
|
||||
toggleNode: async (
|
||||
nodeUuid: string,
|
||||
): Promise<{ success: boolean; message: string; is_disabled: boolean }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get top referrers
|
||||
getTopReferrers: async (limit: number = 20): Promise<TopReferrersResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/stats/referrals/top', { params: { limit } })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/stats/referrals/top', {
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get top campaigns
|
||||
getTopCampaigns: async (limit: number = 20): Promise<TopCampaignsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/stats/campaigns/top', { params: { limit } })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/stats/campaigns/top', {
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get recent payments
|
||||
getRecentPayments: async (limit: number = 50): Promise<RecentPaymentsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/stats/payments/recent', { params: { limit } })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/stats/payments/recent', {
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,220 +1,240 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface LocalizedText {
|
||||
en: string
|
||||
ru: string
|
||||
zh?: string
|
||||
fa?: string
|
||||
fr?: string
|
||||
en: string;
|
||||
ru: string;
|
||||
zh?: string;
|
||||
fa?: string;
|
||||
fr?: string;
|
||||
}
|
||||
|
||||
export interface AppButton {
|
||||
id?: string // Unique identifier for React key (client-side only)
|
||||
buttonLink: string
|
||||
buttonText: LocalizedText
|
||||
id?: string; // Unique identifier for React key (client-side only)
|
||||
buttonLink: string;
|
||||
buttonText: LocalizedText;
|
||||
}
|
||||
|
||||
export interface AppStep {
|
||||
description: LocalizedText
|
||||
buttons?: AppButton[]
|
||||
title?: LocalizedText
|
||||
description: LocalizedText;
|
||||
buttons?: AppButton[];
|
||||
title?: LocalizedText;
|
||||
}
|
||||
|
||||
export interface AppDefinition {
|
||||
id: string
|
||||
name: string
|
||||
isFeatured: boolean
|
||||
urlScheme: string
|
||||
isNeedBase64Encoding?: boolean
|
||||
installationStep: AppStep
|
||||
addSubscriptionStep: AppStep
|
||||
connectAndUseStep: AppStep
|
||||
additionalBeforeAddSubscriptionStep?: AppStep
|
||||
additionalAfterAddSubscriptionStep?: AppStep
|
||||
id: string;
|
||||
name: string;
|
||||
isFeatured: boolean;
|
||||
urlScheme: string;
|
||||
isNeedBase64Encoding?: boolean;
|
||||
installationStep: AppStep;
|
||||
addSubscriptionStep: AppStep;
|
||||
connectAndUseStep: AppStep;
|
||||
additionalBeforeAddSubscriptionStep?: AppStep;
|
||||
additionalAfterAddSubscriptionStep?: AppStep;
|
||||
}
|
||||
|
||||
export interface AppConfigBranding {
|
||||
name: string
|
||||
logoUrl: string
|
||||
supportUrl: string
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
supportUrl: string;
|
||||
}
|
||||
|
||||
export interface AppConfigConfig {
|
||||
additionalLocales: string[]
|
||||
branding: AppConfigBranding
|
||||
additionalLocales: string[];
|
||||
branding: AppConfigBranding;
|
||||
}
|
||||
|
||||
export interface AppConfigResponse {
|
||||
config: AppConfigConfig
|
||||
platforms: Record<string, AppDefinition[]>
|
||||
config: AppConfigConfig;
|
||||
platforms: Record<string, AppDefinition[]>;
|
||||
}
|
||||
|
||||
export const adminAppsApi = {
|
||||
// Get full app config
|
||||
getConfig: async (): Promise<AppConfigResponse> => {
|
||||
const response = await apiClient.get<AppConfigResponse>('/cabinet/admin/apps')
|
||||
return response.data
|
||||
const response = await apiClient.get<AppConfigResponse>('/cabinet/admin/apps');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available platforms
|
||||
getPlatforms: async (): Promise<string[]> => {
|
||||
const response = await apiClient.get<string[]>('/cabinet/admin/apps/platforms')
|
||||
return response.data
|
||||
const response = await apiClient.get<string[]>('/cabinet/admin/apps/platforms');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get apps for a platform
|
||||
getPlatformApps: async (platform: string): Promise<AppDefinition[]> => {
|
||||
const response = await apiClient.get<AppDefinition[]>(`/cabinet/admin/apps/platforms/${platform}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<AppDefinition[]>(
|
||||
`/cabinet/admin/apps/platforms/${platform}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create a new app
|
||||
createApp: async (platform: string, app: AppDefinition): Promise<AppDefinition> => {
|
||||
const response = await apiClient.post<AppDefinition>(`/cabinet/admin/apps/platforms/${platform}`, {
|
||||
platform,
|
||||
app,
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.post<AppDefinition>(
|
||||
`/cabinet/admin/apps/platforms/${platform}`,
|
||||
{
|
||||
platform,
|
||||
app,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update an app
|
||||
updateApp: async (platform: string, appId: string, app: AppDefinition): Promise<AppDefinition> => {
|
||||
const response = await apiClient.put<AppDefinition>(`/cabinet/admin/apps/platforms/${platform}/${appId}`, {
|
||||
app,
|
||||
})
|
||||
return response.data
|
||||
updateApp: async (
|
||||
platform: string,
|
||||
appId: string,
|
||||
app: AppDefinition,
|
||||
): Promise<AppDefinition> => {
|
||||
const response = await apiClient.put<AppDefinition>(
|
||||
`/cabinet/admin/apps/platforms/${platform}/${appId}`,
|
||||
{
|
||||
app,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete an app
|
||||
deleteApp: async (platform: string, appId: string): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/apps/platforms/${platform}/${appId}`)
|
||||
await apiClient.delete(`/cabinet/admin/apps/platforms/${platform}/${appId}`);
|
||||
},
|
||||
|
||||
// Reorder apps
|
||||
reorderApps: async (platform: string, appIds: string[]): Promise<void> => {
|
||||
await apiClient.post(`/cabinet/admin/apps/platforms/${platform}/reorder`, {
|
||||
app_ids: appIds,
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
// Copy app to another platform
|
||||
copyApp: async (platform: string, appId: string, targetPlatform: string): Promise<{ new_id: string }> => {
|
||||
copyApp: async (
|
||||
platform: string,
|
||||
appId: string,
|
||||
targetPlatform: string,
|
||||
): Promise<{ new_id: string }> => {
|
||||
const response = await apiClient.post<{ new_id: string; target_platform: string }>(
|
||||
`/cabinet/admin/apps/platforms/${platform}/copy/${appId}?target_platform=${targetPlatform}`
|
||||
)
|
||||
return response.data
|
||||
`/cabinet/admin/apps/platforms/${platform}/copy/${appId}?target_platform=${targetPlatform}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get branding
|
||||
getBranding: async (): Promise<AppConfigBranding> => {
|
||||
const response = await apiClient.get<AppConfigBranding>('/cabinet/admin/apps/branding')
|
||||
return response.data
|
||||
const response = await apiClient.get<AppConfigBranding>('/cabinet/admin/apps/branding');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update branding
|
||||
updateBranding: async (branding: AppConfigBranding): Promise<AppConfigBranding> => {
|
||||
const response = await apiClient.put<AppConfigBranding>('/cabinet/admin/apps/branding', {
|
||||
branding,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get RemnaWave config status
|
||||
getRemnaWaveStatus: async (): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
const response = await apiClient.get<{ enabled: boolean; config_uuid: string | null }>(
|
||||
'/cabinet/admin/apps/remnawave/status'
|
||||
)
|
||||
return response.data
|
||||
'/cabinet/admin/apps/remnawave/status',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Set RemnaWave config UUID
|
||||
setRemnaWaveUuid: async (uuid: string | null): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
setRemnaWaveUuid: async (
|
||||
uuid: string | null,
|
||||
): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
const response = await apiClient.put<{ enabled: boolean; config_uuid: string | null }>(
|
||||
'/cabinet/admin/apps/remnawave/uuid',
|
||||
{ uuid }
|
||||
)
|
||||
return response.data
|
||||
{ uuid },
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// List available RemnaWave configs
|
||||
listRemnaWaveConfigs: async (): Promise<{ uuid: string; name: string; view_position: number }[]> => {
|
||||
listRemnaWaveConfigs: async (): Promise<
|
||||
{ uuid: string; name: string; view_position: number }[]
|
||||
> => {
|
||||
const response = await apiClient.get<{ uuid: string; name: string; view_position: number }[]>(
|
||||
'/cabinet/admin/apps/remnawave/configs'
|
||||
)
|
||||
return response.data
|
||||
'/cabinet/admin/apps/remnawave/configs',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get RemnaWave subscription config
|
||||
getRemnaWaveConfig: async (): Promise<RemnawaveConfig> => {
|
||||
const response = await apiClient.get<{
|
||||
uuid: string
|
||||
name: string
|
||||
view_position: number
|
||||
config: RemnawaveConfig
|
||||
}>('/cabinet/admin/apps/remnawave/config')
|
||||
return response.data.config
|
||||
uuid: string;
|
||||
name: string;
|
||||
view_position: number;
|
||||
config: RemnawaveConfig;
|
||||
}>('/cabinet/admin/apps/remnawave/config');
|
||||
return response.data.config;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// ============== RemnaWave Format Types ==============
|
||||
|
||||
export interface RemnawaveButton {
|
||||
url: string
|
||||
text: LocalizedText
|
||||
url: string;
|
||||
text: LocalizedText;
|
||||
}
|
||||
|
||||
export interface RemnawaveBlock {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
buttons?: RemnawaveButton[]
|
||||
svgIconKey?: string
|
||||
svgIconColor?: string
|
||||
title: LocalizedText;
|
||||
description: LocalizedText;
|
||||
buttons?: RemnawaveButton[];
|
||||
svgIconKey?: string;
|
||||
svgIconColor?: string;
|
||||
}
|
||||
|
||||
export interface RemnawaveApp {
|
||||
name: string
|
||||
featured?: boolean
|
||||
urlScheme?: string
|
||||
isNeedBase64Encoding?: boolean
|
||||
blocks: RemnawaveBlock[]
|
||||
name: string;
|
||||
featured?: boolean;
|
||||
urlScheme?: string;
|
||||
isNeedBase64Encoding?: boolean;
|
||||
blocks: RemnawaveBlock[];
|
||||
}
|
||||
|
||||
export interface RemnawavePlatform {
|
||||
apps: RemnawaveApp[]
|
||||
apps: RemnawaveApp[];
|
||||
}
|
||||
|
||||
export interface RemnawaveSvgItem {
|
||||
svgString: string
|
||||
tags?: string[]
|
||||
svgString: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface RemnawaveBaseSettings {
|
||||
isShowTutorialButton: boolean
|
||||
tutorialUrl: string
|
||||
isShowTutorialButton: boolean;
|
||||
tutorialUrl: string;
|
||||
}
|
||||
|
||||
export interface RemnawaveBaseTranslations {
|
||||
installApp: LocalizedText
|
||||
addSubscription: LocalizedText
|
||||
connectAndUse: LocalizedText
|
||||
copyLink: LocalizedText
|
||||
openApp: LocalizedText
|
||||
tutorial: LocalizedText
|
||||
close: LocalizedText
|
||||
installApp: LocalizedText;
|
||||
addSubscription: LocalizedText;
|
||||
connectAndUse: LocalizedText;
|
||||
copyLink: LocalizedText;
|
||||
openApp: LocalizedText;
|
||||
tutorial: LocalizedText;
|
||||
close: LocalizedText;
|
||||
}
|
||||
|
||||
export interface RemnawaveBrandingSettings {
|
||||
name: string
|
||||
logoUrl: string
|
||||
supportUrl: string
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
supportUrl: string;
|
||||
}
|
||||
|
||||
export interface RemnawaveConfig {
|
||||
platforms: Record<string, RemnawavePlatform>
|
||||
svgLibrary?: Record<string, RemnawaveSvgItem>
|
||||
baseSettings?: RemnawaveBaseSettings
|
||||
baseTranslations?: RemnawaveBaseTranslations
|
||||
brandingSettings?: RemnawaveBrandingSettings
|
||||
platforms: Record<string, RemnawavePlatform>;
|
||||
svgLibrary?: Record<string, RemnawaveSvgItem>;
|
||||
baseSettings?: RemnawaveBaseSettings;
|
||||
baseTranslations?: RemnawaveBaseTranslations;
|
||||
brandingSettings?: RemnawaveBrandingSettings;
|
||||
}
|
||||
|
||||
// ============== Converter Functions ==============
|
||||
@@ -225,23 +245,29 @@ const emptyLocalizedText = (): LocalizedText => ({
|
||||
zh: '',
|
||||
fa: '',
|
||||
fr: '',
|
||||
})
|
||||
});
|
||||
|
||||
// Convert Cabinet button to RemnaWave button
|
||||
const cabinetButtonToRemnawave = (button: AppButton): RemnawaveButton => ({
|
||||
url: button.buttonLink,
|
||||
text: { ...emptyLocalizedText(), ...button.buttonText },
|
||||
})
|
||||
});
|
||||
|
||||
// Convert RemnaWave button to Cabinet button
|
||||
const remnawaveButtonToCabinet = (button: RemnawaveButton): AppButton => ({
|
||||
buttonLink: button.url,
|
||||
buttonText: { en: button.text.en || '', ru: button.text.ru || '', zh: button.text.zh, fa: button.text.fa, fr: button.text.fr },
|
||||
})
|
||||
buttonText: {
|
||||
en: button.text.en || '',
|
||||
ru: button.text.ru || '',
|
||||
zh: button.text.zh,
|
||||
fa: button.text.fa,
|
||||
fr: button.text.fr,
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Cabinet app to RemnaWave app format
|
||||
export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
const blocks: RemnawaveBlock[] = []
|
||||
const blocks: RemnawaveBlock[] = [];
|
||||
|
||||
// Block 1: Installation
|
||||
blocks.push({
|
||||
@@ -250,17 +276,27 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
buttons: app.installationStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'download',
|
||||
svgIconColor: '#3B82F6',
|
||||
})
|
||||
});
|
||||
|
||||
// Block 2 (optional): Additional before subscription
|
||||
if (app.additionalBeforeAddSubscriptionStep?.description?.en || app.additionalBeforeAddSubscriptionStep?.description?.ru) {
|
||||
if (
|
||||
app.additionalBeforeAddSubscriptionStep?.description?.en ||
|
||||
app.additionalBeforeAddSubscriptionStep?.description?.ru
|
||||
) {
|
||||
blocks.push({
|
||||
title: app.additionalBeforeAddSubscriptionStep.title || { ...emptyLocalizedText(), en: 'Preparation', ru: 'Подготовка' },
|
||||
description: { ...emptyLocalizedText(), ...app.additionalBeforeAddSubscriptionStep.description },
|
||||
title: app.additionalBeforeAddSubscriptionStep.title || {
|
||||
...emptyLocalizedText(),
|
||||
en: 'Preparation',
|
||||
ru: 'Подготовка',
|
||||
},
|
||||
description: {
|
||||
...emptyLocalizedText(),
|
||||
...app.additionalBeforeAddSubscriptionStep.description,
|
||||
},
|
||||
buttons: app.additionalBeforeAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'settings',
|
||||
svgIconColor: '#8B5CF6',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Block 3: Add subscription
|
||||
@@ -270,17 +306,27 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
buttons: app.addSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'plus',
|
||||
svgIconColor: '#10B981',
|
||||
})
|
||||
});
|
||||
|
||||
// Block 4 (optional): Additional after subscription
|
||||
if (app.additionalAfterAddSubscriptionStep?.description?.en || app.additionalAfterAddSubscriptionStep?.description?.ru) {
|
||||
if (
|
||||
app.additionalAfterAddSubscriptionStep?.description?.en ||
|
||||
app.additionalAfterAddSubscriptionStep?.description?.ru
|
||||
) {
|
||||
blocks.push({
|
||||
title: app.additionalAfterAddSubscriptionStep.title || { ...emptyLocalizedText(), en: 'Configuration', ru: 'Настройка' },
|
||||
description: { ...emptyLocalizedText(), ...app.additionalAfterAddSubscriptionStep.description },
|
||||
title: app.additionalAfterAddSubscriptionStep.title || {
|
||||
...emptyLocalizedText(),
|
||||
en: 'Configuration',
|
||||
ru: 'Настройка',
|
||||
},
|
||||
description: {
|
||||
...emptyLocalizedText(),
|
||||
...app.additionalAfterAddSubscriptionStep.description,
|
||||
},
|
||||
buttons: app.additionalAfterAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'settings',
|
||||
svgIconColor: '#F59E0B',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Block 5: Connect and use
|
||||
@@ -290,7 +336,7 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
buttons: app.connectAndUseStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'check',
|
||||
svgIconColor: '#22C55E',
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
name: app.name,
|
||||
@@ -298,78 +344,104 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
urlScheme: app.urlScheme,
|
||||
isNeedBase64Encoding: app.isNeedBase64Encoding,
|
||||
blocks,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Convert RemnaWave app to Cabinet app format
|
||||
export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppDefinition => {
|
||||
const blocks = app.blocks || []
|
||||
const blocks = app.blocks || [];
|
||||
|
||||
// Default empty step
|
||||
const emptyStep = (): AppStep => ({
|
||||
description: emptyLocalizedText(),
|
||||
buttons: [],
|
||||
})
|
||||
});
|
||||
|
||||
// Try to identify blocks by their titles or position
|
||||
let installationBlock: RemnawaveBlock | undefined
|
||||
let subscriptionBlock: RemnawaveBlock | undefined
|
||||
let connectBlock: RemnawaveBlock | undefined
|
||||
let beforeSubBlock: RemnawaveBlock | undefined
|
||||
let afterSubBlock: RemnawaveBlock | undefined
|
||||
let installationBlock: RemnawaveBlock | undefined;
|
||||
let subscriptionBlock: RemnawaveBlock | undefined;
|
||||
let connectBlock: RemnawaveBlock | undefined;
|
||||
let beforeSubBlock: RemnawaveBlock | undefined;
|
||||
let afterSubBlock: RemnawaveBlock | undefined;
|
||||
|
||||
// First pass: try to identify by title keywords
|
||||
for (const block of blocks) {
|
||||
const enTitle = (block.title?.en || '').toLowerCase()
|
||||
const ruTitle = (block.title?.ru || '').toLowerCase()
|
||||
const enTitle = (block.title?.en || '').toLowerCase();
|
||||
const ruTitle = (block.title?.ru || '').toLowerCase();
|
||||
|
||||
if (enTitle.includes('install') || ruTitle.includes('установ') || ruTitle.includes('скачай')) {
|
||||
if (!installationBlock) installationBlock = block
|
||||
} else if (enTitle.includes('subscription') || enTitle.includes('add') || ruTitle.includes('подписк') || ruTitle.includes('добав')) {
|
||||
if (!subscriptionBlock) subscriptionBlock = block
|
||||
} else if (enTitle.includes('connect') || enTitle.includes('use') || ruTitle.includes('подключ') || ruTitle.includes('использ')) {
|
||||
if (!connectBlock) connectBlock = block
|
||||
if (!installationBlock) installationBlock = block;
|
||||
} else if (
|
||||
enTitle.includes('subscription') ||
|
||||
enTitle.includes('add') ||
|
||||
ruTitle.includes('подписк') ||
|
||||
ruTitle.includes('добав')
|
||||
) {
|
||||
if (!subscriptionBlock) subscriptionBlock = block;
|
||||
} else if (
|
||||
enTitle.includes('connect') ||
|
||||
enTitle.includes('use') ||
|
||||
ruTitle.includes('подключ') ||
|
||||
ruTitle.includes('использ')
|
||||
) {
|
||||
if (!connectBlock) connectBlock = block;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign remaining blocks by position if not found by title
|
||||
if (!installationBlock && blocks.length > 0) {
|
||||
installationBlock = blocks[0]
|
||||
installationBlock = blocks[0];
|
||||
}
|
||||
if (!subscriptionBlock && blocks.length > 1) {
|
||||
subscriptionBlock = blocks.find(b => b !== installationBlock) || blocks[1]
|
||||
subscriptionBlock = blocks.find((b) => b !== installationBlock) || blocks[1];
|
||||
}
|
||||
if (!connectBlock && blocks.length > 2) {
|
||||
connectBlock = blocks.find(b => b !== installationBlock && b !== subscriptionBlock) || blocks[blocks.length - 1]
|
||||
connectBlock =
|
||||
blocks.find((b) => b !== installationBlock && b !== subscriptionBlock) ||
|
||||
blocks[blocks.length - 1];
|
||||
}
|
||||
|
||||
// Assign additional blocks
|
||||
const additionalBlocks = blocks.filter(b =>
|
||||
b !== installationBlock && b !== subscriptionBlock && b !== connectBlock
|
||||
)
|
||||
const additionalBlocks = blocks.filter(
|
||||
(b) => b !== installationBlock && b !== subscriptionBlock && b !== connectBlock,
|
||||
);
|
||||
if (additionalBlocks.length >= 1) {
|
||||
// Check if block appears before subscription
|
||||
const subIndex = blocks.indexOf(subscriptionBlock!)
|
||||
const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0])
|
||||
const subIndex = blocks.indexOf(subscriptionBlock!);
|
||||
const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0]);
|
||||
if (firstAdditionalIndex < subIndex) {
|
||||
beforeSubBlock = additionalBlocks[0]
|
||||
beforeSubBlock = additionalBlocks[0];
|
||||
if (additionalBlocks.length >= 2) {
|
||||
afterSubBlock = additionalBlocks[1]
|
||||
afterSubBlock = additionalBlocks[1];
|
||||
}
|
||||
} else {
|
||||
afterSubBlock = additionalBlocks[0]
|
||||
afterSubBlock = additionalBlocks[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert block to cabinet step
|
||||
const blockToStep = (block: RemnawaveBlock | undefined): AppStep => {
|
||||
if (!block) return emptyStep()
|
||||
if (!block) return emptyStep();
|
||||
return {
|
||||
description: { en: block.description?.en || '', ru: block.description?.ru || '', zh: block.description?.zh, fa: block.description?.fa, fr: block.description?.fr },
|
||||
title: block.title ? { en: block.title.en || '', ru: block.title.ru || '', zh: block.title.zh, fa: block.title.fa, fr: block.title.fr } : undefined,
|
||||
description: {
|
||||
en: block.description?.en || '',
|
||||
ru: block.description?.ru || '',
|
||||
zh: block.description?.zh,
|
||||
fa: block.description?.fa,
|
||||
fr: block.description?.fr,
|
||||
},
|
||||
title: block.title
|
||||
? {
|
||||
en: block.title.en || '',
|
||||
ru: block.title.ru || '',
|
||||
zh: block.title.zh,
|
||||
fa: block.title.fa,
|
||||
fr: block.title.fr,
|
||||
}
|
||||
: undefined,
|
||||
buttons: block.buttons?.map(remnawaveButtonToCabinet),
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
id: `${app.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${platform}-${Date.now()}`,
|
||||
@@ -382,66 +454,114 @@ export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppD
|
||||
connectAndUseStep: blockToStep(connectBlock),
|
||||
additionalBeforeAddSubscriptionStep: beforeSubBlock ? blockToStep(beforeSubBlock) : undefined,
|
||||
additionalAfterAddSubscriptionStep: afterSubBlock ? blockToStep(afterSubBlock) : undefined,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Export all apps from cabinet to RemnaWave format
|
||||
export const exportToRemnawaveFormat = (
|
||||
platformApps: Record<string, AppDefinition[]>,
|
||||
branding?: AppConfigBranding
|
||||
branding?: AppConfigBranding,
|
||||
): RemnawaveConfig => {
|
||||
const platforms: Record<string, RemnawavePlatform> = {}
|
||||
const platforms: Record<string, RemnawavePlatform> = {};
|
||||
|
||||
for (const [platform, apps] of Object.entries(platformApps)) {
|
||||
platforms[platform] = {
|
||||
apps: apps.map(cabinetAppToRemnawave),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
platforms,
|
||||
svgLibrary: {
|
||||
download: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>' },
|
||||
plus: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>' },
|
||||
check: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>' },
|
||||
settings: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>' },
|
||||
download: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>',
|
||||
},
|
||||
plus: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>',
|
||||
},
|
||||
check: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>',
|
||||
},
|
||||
settings: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
|
||||
},
|
||||
},
|
||||
baseSettings: {
|
||||
isShowTutorialButton: false,
|
||||
tutorialUrl: '',
|
||||
},
|
||||
baseTranslations: {
|
||||
installApp: { en: 'Install App', ru: 'Установить приложение', zh: '安装应用', fa: 'نصب برنامه', fr: 'Installer l\'application' },
|
||||
addSubscription: { en: 'Add Subscription', ru: 'Добавить подписку', zh: '添加订阅', fa: 'اضافه کردن اشتراک', fr: 'Ajouter un abonnement' },
|
||||
connectAndUse: { en: 'Connect and Use', ru: 'Подключиться и использовать', zh: '连接并使用', fa: 'اتصال و استفاده', fr: 'Connecter et utiliser' },
|
||||
copyLink: { en: 'Copy Link', ru: 'Скопировать ссылку', zh: '复制链接', fa: 'کپی لینک', fr: 'Copier le lien' },
|
||||
openApp: { en: 'Open App', ru: 'Открыть приложение', zh: '打开应用', fa: 'باز کردن برنامه', fr: 'Ouvrir l\'application' },
|
||||
installApp: {
|
||||
en: 'Install App',
|
||||
ru: 'Установить приложение',
|
||||
zh: '安装应用',
|
||||
fa: 'نصب برنامه',
|
||||
fr: "Installer l'application",
|
||||
},
|
||||
addSubscription: {
|
||||
en: 'Add Subscription',
|
||||
ru: 'Добавить подписку',
|
||||
zh: '添加订阅',
|
||||
fa: 'اضافه کردن اشتراک',
|
||||
fr: 'Ajouter un abonnement',
|
||||
},
|
||||
connectAndUse: {
|
||||
en: 'Connect and Use',
|
||||
ru: 'Подключиться и использовать',
|
||||
zh: '连接并使用',
|
||||
fa: 'اتصال و استفاده',
|
||||
fr: 'Connecter et utiliser',
|
||||
},
|
||||
copyLink: {
|
||||
en: 'Copy Link',
|
||||
ru: 'Скопировать ссылку',
|
||||
zh: '复制链接',
|
||||
fa: 'کپی لینک',
|
||||
fr: 'Copier le lien',
|
||||
},
|
||||
openApp: {
|
||||
en: 'Open App',
|
||||
ru: 'Открыть приложение',
|
||||
zh: '打开应用',
|
||||
fa: 'باز کردن برنامه',
|
||||
fr: "Ouvrir l'application",
|
||||
},
|
||||
tutorial: { en: 'Tutorial', ru: 'Инструкция', zh: '教程', fa: 'آموزش', fr: 'Tutoriel' },
|
||||
close: { en: 'Close', ru: 'Закрыть', zh: '关闭', fa: 'بستن', fr: 'Fermer' },
|
||||
},
|
||||
brandingSettings: branding ? {
|
||||
name: branding.name,
|
||||
logoUrl: branding.logoUrl,
|
||||
supportUrl: branding.supportUrl,
|
||||
} : undefined,
|
||||
}
|
||||
}
|
||||
brandingSettings: branding
|
||||
? {
|
||||
name: branding.name,
|
||||
logoUrl: branding.logoUrl,
|
||||
supportUrl: branding.supportUrl,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Import apps from RemnaWave format to cabinet
|
||||
export const importFromRemnawaveFormat = (
|
||||
config: RemnawaveConfig
|
||||
config: RemnawaveConfig,
|
||||
): { platformApps: Record<string, AppDefinition[]>; branding?: AppConfigBranding } => {
|
||||
const platformApps: Record<string, AppDefinition[]> = {}
|
||||
const platformApps: Record<string, AppDefinition[]> = {};
|
||||
|
||||
for (const [platform, platformData] of Object.entries(config.platforms || {})) {
|
||||
platformApps[platform] = (platformData.apps || []).map(app => remnawaveAppToCabinet(app, platform))
|
||||
platformApps[platform] = (platformData.apps || []).map((app) =>
|
||||
remnawaveAppToCabinet(app, platform),
|
||||
);
|
||||
}
|
||||
|
||||
const branding = config.brandingSettings ? {
|
||||
name: config.brandingSettings.name,
|
||||
logoUrl: config.brandingSettings.logoUrl,
|
||||
supportUrl: config.brandingSettings.supportUrl,
|
||||
} : undefined
|
||||
const branding = config.brandingSettings
|
||||
? {
|
||||
name: config.brandingSettings.name,
|
||||
logoUrl: config.brandingSettings.logoUrl,
|
||||
supportUrl: config.brandingSettings.supportUrl,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return { platformApps, branding }
|
||||
}
|
||||
return { platformApps, branding };
|
||||
};
|
||||
|
||||
@@ -1,168 +1,187 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// Types
|
||||
export interface BroadcastFilter {
|
||||
key: string
|
||||
label: string
|
||||
count: number | null
|
||||
group: string | null
|
||||
key: string;
|
||||
label: string;
|
||||
count: number | null;
|
||||
group: string | null;
|
||||
}
|
||||
|
||||
export interface TariffFilter {
|
||||
key: string
|
||||
label: string
|
||||
tariff_id: number
|
||||
count: number
|
||||
key: string;
|
||||
label: string;
|
||||
tariff_id: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface BroadcastFiltersResponse {
|
||||
filters: BroadcastFilter[]
|
||||
tariff_filters: TariffFilter[]
|
||||
custom_filters: BroadcastFilter[]
|
||||
filters: BroadcastFilter[];
|
||||
tariff_filters: TariffFilter[];
|
||||
custom_filters: BroadcastFilter[];
|
||||
}
|
||||
|
||||
export interface TariffForBroadcast {
|
||||
id: number
|
||||
name: string
|
||||
filter_key: string
|
||||
active_users_count: number
|
||||
id: number;
|
||||
name: string;
|
||||
filter_key: string;
|
||||
active_users_count: number;
|
||||
}
|
||||
|
||||
export interface BroadcastTariffsResponse {
|
||||
tariffs: TariffForBroadcast[]
|
||||
tariffs: TariffForBroadcast[];
|
||||
}
|
||||
|
||||
export interface BroadcastButton {
|
||||
key: string
|
||||
label: string
|
||||
default: boolean
|
||||
key: string;
|
||||
label: string;
|
||||
default: boolean;
|
||||
}
|
||||
|
||||
export interface BroadcastButtonsResponse {
|
||||
buttons: BroadcastButton[]
|
||||
buttons: BroadcastButton[];
|
||||
}
|
||||
|
||||
export interface BroadcastMedia {
|
||||
type: 'photo' | 'video' | 'document'
|
||||
file_id: string
|
||||
caption?: string
|
||||
type: 'photo' | 'video' | 'document';
|
||||
file_id: string;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export interface BroadcastCreateRequest {
|
||||
target: string
|
||||
message_text: string
|
||||
selected_buttons: string[]
|
||||
media?: BroadcastMedia
|
||||
target: string;
|
||||
message_text: string;
|
||||
selected_buttons: string[];
|
||||
media?: BroadcastMedia;
|
||||
}
|
||||
|
||||
export interface Broadcast {
|
||||
id: number
|
||||
target_type: string
|
||||
message_text: string
|
||||
has_media: boolean
|
||||
media_type: string | null
|
||||
media_file_id: string | null
|
||||
media_caption: string | null
|
||||
total_count: number
|
||||
sent_count: number
|
||||
failed_count: number
|
||||
status: 'queued' | 'in_progress' | 'completed' | 'partial' | 'failed' | 'cancelled' | 'cancelling'
|
||||
admin_id: number | null
|
||||
admin_name: string | null
|
||||
created_at: string
|
||||
completed_at: string | null
|
||||
progress_percent: number
|
||||
id: number;
|
||||
target_type: string;
|
||||
message_text: string;
|
||||
has_media: boolean;
|
||||
media_type: string | null;
|
||||
media_file_id: string | null;
|
||||
media_caption: string | null;
|
||||
total_count: number;
|
||||
sent_count: number;
|
||||
failed_count: number;
|
||||
status:
|
||||
| 'queued'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'partial'
|
||||
| 'failed'
|
||||
| 'cancelled'
|
||||
| 'cancelling';
|
||||
admin_id: number | null;
|
||||
admin_name: string | null;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
progress_percent: number;
|
||||
}
|
||||
|
||||
export interface BroadcastListResponse {
|
||||
items: Broadcast[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
items: Broadcast[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface BroadcastPreviewRequest {
|
||||
target: string
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface BroadcastPreviewResponse {
|
||||
target: string
|
||||
count: number
|
||||
target: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MediaUploadResponse {
|
||||
media_type: string
|
||||
file_id: string
|
||||
file_unique_id: string | null
|
||||
media_url: string
|
||||
media_type: string;
|
||||
file_id: string;
|
||||
file_unique_id: string | null;
|
||||
media_url: string;
|
||||
}
|
||||
|
||||
export const adminBroadcastsApi = {
|
||||
// Get all available filters with counts
|
||||
getFilters: async (): Promise<BroadcastFiltersResponse> => {
|
||||
const response = await apiClient.get<BroadcastFiltersResponse>('/cabinet/admin/broadcasts/filters')
|
||||
return response.data
|
||||
const response = await apiClient.get<BroadcastFiltersResponse>(
|
||||
'/cabinet/admin/broadcasts/filters',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get tariffs for filtering
|
||||
getTariffs: async (): Promise<BroadcastTariffsResponse> => {
|
||||
const response = await apiClient.get<BroadcastTariffsResponse>('/cabinet/admin/broadcasts/tariffs')
|
||||
return response.data
|
||||
const response = await apiClient.get<BroadcastTariffsResponse>(
|
||||
'/cabinet/admin/broadcasts/tariffs',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available buttons
|
||||
getButtons: async (): Promise<BroadcastButtonsResponse> => {
|
||||
const response = await apiClient.get<BroadcastButtonsResponse>('/cabinet/admin/broadcasts/buttons')
|
||||
return response.data
|
||||
const response = await apiClient.get<BroadcastButtonsResponse>(
|
||||
'/cabinet/admin/broadcasts/buttons',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Preview broadcast (get recipients count)
|
||||
preview: async (target: string): Promise<BroadcastPreviewResponse> => {
|
||||
const response = await apiClient.post<BroadcastPreviewResponse>('/cabinet/admin/broadcasts/preview', {
|
||||
target,
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.post<BroadcastPreviewResponse>(
|
||||
'/cabinet/admin/broadcasts/preview',
|
||||
{
|
||||
target,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create and start broadcast
|
||||
create: async (data: BroadcastCreateRequest): Promise<Broadcast> => {
|
||||
const response = await apiClient.post<Broadcast>('/cabinet/admin/broadcasts', data)
|
||||
return response.data
|
||||
const response = await apiClient.post<Broadcast>('/cabinet/admin/broadcasts', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get list of broadcasts
|
||||
list: async (limit = 20, offset = 0): Promise<BroadcastListResponse> => {
|
||||
const response = await apiClient.get<BroadcastListResponse>('/cabinet/admin/broadcasts', {
|
||||
params: { limit, offset },
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get broadcast details
|
||||
get: async (id: number): Promise<Broadcast> => {
|
||||
const response = await apiClient.get<Broadcast>(`/cabinet/admin/broadcasts/${id}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<Broadcast>(`/cabinet/admin/broadcasts/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Stop broadcast
|
||||
stop: async (id: number): Promise<Broadcast> => {
|
||||
const response = await apiClient.post<Broadcast>(`/cabinet/admin/broadcasts/${id}/stop`)
|
||||
return response.data
|
||||
const response = await apiClient.post<Broadcast>(`/cabinet/admin/broadcasts/${id}/stop`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Upload media (uses existing media endpoint)
|
||||
uploadMedia: async (file: File, mediaType: 'photo' | 'video' | 'document'): Promise<MediaUploadResponse> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('media_type', mediaType)
|
||||
uploadMedia: async (
|
||||
file: File,
|
||||
mediaType: 'photo' | 'video' | 'document',
|
||||
): Promise<MediaUploadResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('media_type', mediaType);
|
||||
|
||||
const response = await apiClient.post<MediaUploadResponse>('/cabinet/media/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default adminBroadcastsApi
|
||||
export default adminBroadcastsApi;
|
||||
|
||||
@@ -1,85 +1,105 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface EmailTemplateLanguageStatus {
|
||||
has_custom: boolean
|
||||
has_custom: boolean;
|
||||
}
|
||||
|
||||
export interface EmailTemplateType {
|
||||
type: string
|
||||
label: Record<string, string>
|
||||
description: Record<string, string>
|
||||
context_vars: string[]
|
||||
languages: Record<string, EmailTemplateLanguageStatus>
|
||||
type: string;
|
||||
label: Record<string, string>;
|
||||
description: Record<string, string>;
|
||||
context_vars: string[];
|
||||
languages: Record<string, EmailTemplateLanguageStatus>;
|
||||
}
|
||||
|
||||
export interface EmailTemplateListResponse {
|
||||
items: EmailTemplateType[]
|
||||
available_languages: string[]
|
||||
items: EmailTemplateType[];
|
||||
available_languages: string[];
|
||||
}
|
||||
|
||||
export interface EmailTemplateLanguageData {
|
||||
subject: string
|
||||
body_html: string
|
||||
is_default: boolean
|
||||
default_subject: string
|
||||
default_body_html: string
|
||||
subject: string;
|
||||
body_html: string;
|
||||
is_default: boolean;
|
||||
default_subject: string;
|
||||
default_body_html: string;
|
||||
}
|
||||
|
||||
export interface EmailTemplateDetail {
|
||||
notification_type: string
|
||||
label: Record<string, string>
|
||||
description: Record<string, string>
|
||||
context_vars: string[]
|
||||
languages: Record<string, EmailTemplateLanguageData>
|
||||
notification_type: string;
|
||||
label: Record<string, string>;
|
||||
description: Record<string, string>;
|
||||
context_vars: string[];
|
||||
languages: Record<string, EmailTemplateLanguageData>;
|
||||
}
|
||||
|
||||
export interface EmailTemplateUpdateRequest {
|
||||
subject: string
|
||||
body_html: string
|
||||
subject: string;
|
||||
body_html: string;
|
||||
}
|
||||
|
||||
export interface EmailTemplatePreviewRequest {
|
||||
language: string
|
||||
subject?: string
|
||||
body_html?: string
|
||||
language: string;
|
||||
subject?: string;
|
||||
body_html?: string;
|
||||
}
|
||||
|
||||
export interface EmailTemplatePreviewResponse {
|
||||
subject: string
|
||||
body_html: string
|
||||
subject: string;
|
||||
body_html: string;
|
||||
}
|
||||
|
||||
export interface EmailTemplateSendTestRequest {
|
||||
language: string
|
||||
email?: string
|
||||
language: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export const adminEmailTemplatesApi = {
|
||||
getTemplateTypes: async (): Promise<EmailTemplateListResponse> => {
|
||||
const response = await apiClient.get<EmailTemplateListResponse>('/cabinet/admin/email-templates')
|
||||
return response.data
|
||||
const response = await apiClient.get<EmailTemplateListResponse>(
|
||||
'/cabinet/admin/email-templates',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTemplate: async (notificationType: string): Promise<EmailTemplateDetail> => {
|
||||
const response = await apiClient.get<EmailTemplateDetail>(`/cabinet/admin/email-templates/${notificationType}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<EmailTemplateDetail>(
|
||||
`/cabinet/admin/email-templates/${notificationType}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateTemplate: async (notificationType: string, language: string, data: EmailTemplateUpdateRequest): Promise<void> => {
|
||||
await apiClient.put(`/cabinet/admin/email-templates/${notificationType}/${language}`, data)
|
||||
updateTemplate: async (
|
||||
notificationType: string,
|
||||
language: string,
|
||||
data: EmailTemplateUpdateRequest,
|
||||
): Promise<void> => {
|
||||
await apiClient.put(`/cabinet/admin/email-templates/${notificationType}/${language}`, data);
|
||||
},
|
||||
|
||||
deleteTemplate: async (notificationType: string, language: string): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/email-templates/${notificationType}/${language}`)
|
||||
await apiClient.delete(`/cabinet/admin/email-templates/${notificationType}/${language}`);
|
||||
},
|
||||
|
||||
previewTemplate: async (notificationType: string, data: EmailTemplatePreviewRequest): Promise<EmailTemplatePreviewResponse> => {
|
||||
const response = await apiClient.post<EmailTemplatePreviewResponse>(`/cabinet/admin/email-templates/${notificationType}/preview`, data)
|
||||
return response.data
|
||||
previewTemplate: async (
|
||||
notificationType: string,
|
||||
data: EmailTemplatePreviewRequest,
|
||||
): Promise<EmailTemplatePreviewResponse> => {
|
||||
const response = await apiClient.post<EmailTemplatePreviewResponse>(
|
||||
`/cabinet/admin/email-templates/${notificationType}/preview`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
sendTestEmail: async (notificationType: string, data: EmailTemplateSendTestRequest): Promise<{ sent_to: string }> => {
|
||||
const response = await apiClient.post<{ status: string; sent_to: string }>(`/cabinet/admin/email-templates/${notificationType}/test`, data)
|
||||
return response.data
|
||||
sendTestEmail: async (
|
||||
notificationType: string,
|
||||
data: EmailTemplateSendTestRequest,
|
||||
): Promise<{ sent_to: string }> => {
|
||||
const response = await apiClient.post<{ status: string; sent_to: string }>(
|
||||
`/cabinet/admin/email-templates/${notificationType}/test`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import apiClient from './client'
|
||||
import type { PaymentMethodConfig, PromoGroupSimple } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { PaymentMethodConfig, PromoGroupSimple } from '../types';
|
||||
|
||||
export const adminPaymentMethodsApi = {
|
||||
getAll: async (): Promise<PaymentMethodConfig[]> => {
|
||||
const response = await apiClient.get<PaymentMethodConfig[]>('/cabinet/admin/payment-methods')
|
||||
return response.data
|
||||
const response = await apiClient.get<PaymentMethodConfig[]>('/cabinet/admin/payment-methods');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getOne: async (methodId: string): Promise<PaymentMethodConfig> => {
|
||||
const response = await apiClient.get<PaymentMethodConfig>(`/cabinet/admin/payment-methods/${methodId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<PaymentMethodConfig>(
|
||||
`/cabinet/admin/payment-methods/${methodId}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (methodId: string, data: Record<string, unknown>): Promise<PaymentMethodConfig> => {
|
||||
const response = await apiClient.put<PaymentMethodConfig>(
|
||||
`/cabinet/admin/payment-methods/${methodId}`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateOrder: async (methodIds: string[]): Promise<void> => {
|
||||
await apiClient.put('/cabinet/admin/payment-methods/order', { method_ids: methodIds })
|
||||
await apiClient.put('/cabinet/admin/payment-methods/order', { method_ids: methodIds });
|
||||
},
|
||||
|
||||
getPromoGroups: async (): Promise<PromoGroupSimple[]> => {
|
||||
const response = await apiClient.get<PromoGroupSimple[]>('/cabinet/admin/payment-methods/promo-groups')
|
||||
return response.data
|
||||
const response = await apiClient.get<PromoGroupSimple[]>(
|
||||
'/cabinet/admin/payment-methods/promo-groups',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
import apiClient from './client'
|
||||
import type { PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types';
|
||||
|
||||
export interface PaymentsStats {
|
||||
total_pending: number
|
||||
by_method: Record<string, number>
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
const response = await apiClient.post<ManualCheckResponse>(
|
||||
`/cabinet/admin/payments/${method}/${paymentId}/check`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,227 +1,227 @@
|
||||
import { apiClient } from './client'
|
||||
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>
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
realtime_download: number;
|
||||
realtime_upload: number;
|
||||
realtime_total: number;
|
||||
}
|
||||
|
||||
export interface TrafficPeriod {
|
||||
current: number
|
||||
previous: number
|
||||
difference?: string
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
items: NodeInfo[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface NodesOverview {
|
||||
total: number
|
||||
online: number
|
||||
offline: number
|
||||
disabled: number
|
||||
total_users_online: number
|
||||
nodes: NodeInfo[]
|
||||
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
|
||||
node: NodeInfo;
|
||||
realtime?: Record<string, unknown>;
|
||||
usage_history: Record<string, unknown>[];
|
||||
last_updated?: string;
|
||||
}
|
||||
|
||||
export interface NodeActionResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
is_disabled?: boolean
|
||||
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
|
||||
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
|
||||
items: SquadWithLocalInfo[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SquadDetailResponse extends SquadWithLocalInfo {
|
||||
description?: string
|
||||
sort_order?: number
|
||||
active_subscriptions: number
|
||||
description?: string;
|
||||
sort_order?: number;
|
||||
active_subscriptions: number;
|
||||
}
|
||||
|
||||
export interface SquadOperationResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: Record<string, unknown>
|
||||
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
|
||||
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
|
||||
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
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
data?: MigrationStats;
|
||||
}
|
||||
|
||||
// Inbounds
|
||||
export interface InboundsListResponse {
|
||||
items: Record<string, unknown>[]
|
||||
total: number
|
||||
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>
|
||||
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
|
||||
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>
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
@@ -229,158 +229,176 @@ export interface SyncResponse {
|
||||
export const adminRemnawaveApi = {
|
||||
// Status & Connection
|
||||
getStatus: async (): Promise<RemnaWaveStatusResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/status')
|
||||
return response.data
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Inbounds
|
||||
getInbounds: async (): Promise<InboundsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/inbounds')
|
||||
return response.data
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
const response = await apiClient.get('/cabinet/admin/remnawave/sync/recommendations');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default adminRemnawaveApi
|
||||
export default adminRemnawaveApi;
|
||||
|
||||
@@ -1,73 +1,79 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface SettingCategoryRef {
|
||||
key: string
|
||||
label: string
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SettingCategorySummary {
|
||||
key: string
|
||||
label: string
|
||||
description: string
|
||||
items: number
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
items: number;
|
||||
}
|
||||
|
||||
export interface SettingChoice {
|
||||
value: unknown
|
||||
label: string
|
||||
description?: string | null
|
||||
value: unknown;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface SettingHint {
|
||||
description: string
|
||||
format: string
|
||||
example: string
|
||||
warning: string
|
||||
description: string;
|
||||
format: string;
|
||||
example: string;
|
||||
warning: string;
|
||||
}
|
||||
|
||||
export interface SettingDefinition {
|
||||
key: string
|
||||
name: string
|
||||
category: SettingCategoryRef
|
||||
type: string
|
||||
is_optional: boolean
|
||||
current: unknown
|
||||
original: unknown
|
||||
has_override: boolean
|
||||
read_only: boolean
|
||||
choices: SettingChoice[]
|
||||
hint?: SettingHint | null
|
||||
key: string;
|
||||
name: string;
|
||||
category: SettingCategoryRef;
|
||||
type: string;
|
||||
is_optional: boolean;
|
||||
current: unknown;
|
||||
original: unknown;
|
||||
has_override: boolean;
|
||||
read_only: boolean;
|
||||
choices: SettingChoice[];
|
||||
hint?: SettingHint | null;
|
||||
}
|
||||
|
||||
export const adminSettingsApi = {
|
||||
// Get list of setting categories
|
||||
getCategories: async (): Promise<SettingCategorySummary[]> => {
|
||||
const response = await apiClient.get<SettingCategorySummary[]>('/cabinet/admin/settings/categories')
|
||||
return response.data
|
||||
const response = await apiClient.get<SettingCategorySummary[]>(
|
||||
'/cabinet/admin/settings/categories',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all settings or settings for a specific category
|
||||
getSettings: async (categoryKey?: string): Promise<SettingDefinition[]> => {
|
||||
const params = categoryKey ? { category_key: categoryKey } : {}
|
||||
const response = await apiClient.get<SettingDefinition[]>('/cabinet/admin/settings', { params })
|
||||
return response.data
|
||||
const params = categoryKey ? { category_key: categoryKey } : {};
|
||||
const response = await apiClient.get<SettingDefinition[]>('/cabinet/admin/settings', {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get a specific setting by key
|
||||
getSetting: async (key: string): Promise<SettingDefinition> => {
|
||||
const response = await apiClient.get<SettingDefinition>(`/cabinet/admin/settings/${key}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<SettingDefinition>(`/cabinet/admin/settings/${key}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update a setting value
|
||||
updateSetting: async (key: string, value: unknown): Promise<SettingDefinition> => {
|
||||
const response = await apiClient.put<SettingDefinition>(`/cabinet/admin/settings/${key}`, { value })
|
||||
return response.data
|
||||
const response = await apiClient.put<SettingDefinition>(`/cabinet/admin/settings/${key}`, {
|
||||
value,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reset a setting to default
|
||||
resetSetting: async (key: string): Promise<SettingDefinition> => {
|
||||
const response = await apiClient.delete<SettingDefinition>(`/cabinet/admin/settings/${key}`)
|
||||
return response.data
|
||||
const response = await apiClient.delete<SettingDefinition>(`/cabinet/admin/settings/${key}`);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,418 +1,478 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
export interface UserSubscriptionInfo {
|
||||
id: number
|
||||
status: string
|
||||
is_trial: boolean
|
||||
start_date: string | null
|
||||
end_date: string | null
|
||||
traffic_limit_gb: number
|
||||
traffic_used_gb: number
|
||||
device_limit: number
|
||||
tariff_id: number | null
|
||||
tariff_name: string | null
|
||||
autopay_enabled: boolean
|
||||
is_active: boolean
|
||||
days_remaining: number
|
||||
id: number;
|
||||
status: string;
|
||||
is_trial: boolean;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
traffic_limit_gb: number;
|
||||
traffic_used_gb: number;
|
||||
device_limit: number;
|
||||
tariff_id: number | null;
|
||||
tariff_name: string | null;
|
||||
autopay_enabled: boolean;
|
||||
is_active: boolean;
|
||||
days_remaining: number;
|
||||
}
|
||||
|
||||
export interface UserPromoGroupInfo {
|
||||
id: number
|
||||
name: string
|
||||
is_default: boolean
|
||||
id: number;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
export interface UserListItem {
|
||||
id: number
|
||||
telegram_id: number
|
||||
username: string | null
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
full_name: string
|
||||
status: string
|
||||
balance_kopeks: number
|
||||
balance_rubles: number
|
||||
created_at: string
|
||||
last_activity: string | null
|
||||
has_subscription: boolean
|
||||
subscription_status: string | null
|
||||
subscription_is_trial: boolean
|
||||
subscription_end_date: string | null
|
||||
promo_group_id: number | null
|
||||
promo_group_name: string | null
|
||||
total_spent_kopeks: number
|
||||
purchase_count: number
|
||||
has_restrictions: boolean
|
||||
restriction_topup: boolean
|
||||
restriction_subscription: boolean
|
||||
id: number;
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
full_name: string;
|
||||
status: string;
|
||||
balance_kopeks: number;
|
||||
balance_rubles: number;
|
||||
created_at: string;
|
||||
last_activity: string | null;
|
||||
has_subscription: boolean;
|
||||
subscription_status: string | null;
|
||||
subscription_is_trial: boolean;
|
||||
subscription_end_date: string | null;
|
||||
promo_group_id: number | null;
|
||||
promo_group_name: string | null;
|
||||
total_spent_kopeks: number;
|
||||
purchase_count: number;
|
||||
has_restrictions: boolean;
|
||||
restriction_topup: boolean;
|
||||
restriction_subscription: boolean;
|
||||
}
|
||||
|
||||
export interface UsersListResponse {
|
||||
users: UserListItem[]
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
users: UserListItem[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface UserTransactionItem {
|
||||
id: number
|
||||
type: string
|
||||
amount_kopeks: number
|
||||
amount_rubles: number
|
||||
description: string | null
|
||||
payment_method: string | null
|
||||
is_completed: boolean
|
||||
created_at: string
|
||||
id: number;
|
||||
type: string;
|
||||
amount_kopeks: number;
|
||||
amount_rubles: number;
|
||||
description: string | null;
|
||||
payment_method: string | null;
|
||||
is_completed: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface UserReferralInfo {
|
||||
referral_code: string
|
||||
referrals_count: number
|
||||
total_earnings_kopeks: number
|
||||
commission_percent: number | null
|
||||
referred_by_id: number | null
|
||||
referred_by_username: string | null
|
||||
referral_code: string;
|
||||
referrals_count: number;
|
||||
total_earnings_kopeks: number;
|
||||
commission_percent: number | null;
|
||||
referred_by_id: number | null;
|
||||
referred_by_username: string | null;
|
||||
}
|
||||
|
||||
export interface UserDetailResponse {
|
||||
id: number
|
||||
telegram_id: number
|
||||
username: string | null
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
full_name: string
|
||||
status: string
|
||||
language: string
|
||||
balance_kopeks: number
|
||||
balance_rubles: number
|
||||
email: string | null
|
||||
email_verified: boolean
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
last_activity: string | null
|
||||
cabinet_last_login: string | null
|
||||
subscription: UserSubscriptionInfo | null
|
||||
promo_group: UserPromoGroupInfo | null
|
||||
referral: UserReferralInfo
|
||||
total_spent_kopeks: number
|
||||
purchase_count: number
|
||||
used_promocodes: number
|
||||
has_had_paid_subscription: boolean
|
||||
lifetime_used_traffic_bytes: number
|
||||
restriction_topup: boolean
|
||||
restriction_subscription: boolean
|
||||
restriction_reason: string | null
|
||||
promo_offer_discount_percent: number
|
||||
promo_offer_discount_source: string | null
|
||||
promo_offer_discount_expires_at: string | null
|
||||
recent_transactions: UserTransactionItem[]
|
||||
remnawave_uuid: string | null
|
||||
id: number;
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
full_name: string;
|
||||
status: string;
|
||||
language: string;
|
||||
balance_kopeks: number;
|
||||
balance_rubles: number;
|
||||
email: string | null;
|
||||
email_verified: boolean;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
last_activity: string | null;
|
||||
cabinet_last_login: string | null;
|
||||
subscription: UserSubscriptionInfo | null;
|
||||
promo_group: UserPromoGroupInfo | null;
|
||||
referral: UserReferralInfo;
|
||||
total_spent_kopeks: number;
|
||||
purchase_count: number;
|
||||
used_promocodes: number;
|
||||
has_had_paid_subscription: boolean;
|
||||
lifetime_used_traffic_bytes: number;
|
||||
restriction_topup: boolean;
|
||||
restriction_subscription: boolean;
|
||||
restriction_reason: string | null;
|
||||
promo_offer_discount_percent: number;
|
||||
promo_offer_discount_source: string | null;
|
||||
promo_offer_discount_expires_at: string | null;
|
||||
recent_transactions: UserTransactionItem[];
|
||||
remnawave_uuid: string | null;
|
||||
}
|
||||
|
||||
export interface UsersStatsResponse {
|
||||
total_users: number
|
||||
active_users: number
|
||||
blocked_users: number
|
||||
deleted_users: number
|
||||
new_today: number
|
||||
new_week: number
|
||||
new_month: number
|
||||
users_with_subscription: number
|
||||
users_with_active_subscription: number
|
||||
users_with_trial: number
|
||||
users_with_expired_subscription: number
|
||||
total_balance_kopeks: number
|
||||
total_balance_rubles: number
|
||||
avg_balance_kopeks: number
|
||||
active_today: number
|
||||
active_week: number
|
||||
active_month: number
|
||||
total_users: number;
|
||||
active_users: number;
|
||||
blocked_users: number;
|
||||
deleted_users: number;
|
||||
new_today: number;
|
||||
new_week: number;
|
||||
new_month: number;
|
||||
users_with_subscription: number;
|
||||
users_with_active_subscription: number;
|
||||
users_with_trial: number;
|
||||
users_with_expired_subscription: number;
|
||||
total_balance_kopeks: number;
|
||||
total_balance_rubles: number;
|
||||
avg_balance_kopeks: number;
|
||||
active_today: number;
|
||||
active_week: number;
|
||||
active_month: number;
|
||||
}
|
||||
|
||||
// Available tariffs
|
||||
export interface PeriodPriceInfo {
|
||||
days: number
|
||||
price_kopeks: number
|
||||
price_rubles: number
|
||||
days: number;
|
||||
price_kopeks: number;
|
||||
price_rubles: number;
|
||||
}
|
||||
|
||||
export interface UserAvailableTariff {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
is_trial_available: boolean
|
||||
traffic_limit_gb: number
|
||||
device_limit: number
|
||||
tier_level: number
|
||||
display_order: number
|
||||
period_prices: PeriodPriceInfo[]
|
||||
is_daily: boolean
|
||||
daily_price_kopeks: number
|
||||
custom_days_enabled: boolean
|
||||
price_per_day_kopeks: number
|
||||
min_days: number
|
||||
max_days: number
|
||||
is_available: boolean
|
||||
requires_promo_group: boolean
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
is_active: boolean;
|
||||
is_trial_available: boolean;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
tier_level: number;
|
||||
display_order: number;
|
||||
period_prices: PeriodPriceInfo[];
|
||||
is_daily: boolean;
|
||||
daily_price_kopeks: number;
|
||||
custom_days_enabled: boolean;
|
||||
price_per_day_kopeks: number;
|
||||
min_days: number;
|
||||
max_days: number;
|
||||
is_available: boolean;
|
||||
requires_promo_group: boolean;
|
||||
}
|
||||
|
||||
export interface UserAvailableTariffsResponse {
|
||||
user_id: number
|
||||
promo_group_id: number | null
|
||||
promo_group_name: string | null
|
||||
tariffs: UserAvailableTariff[]
|
||||
total: number
|
||||
current_tariff_id: number | null
|
||||
current_tariff_name: string | null
|
||||
user_id: number;
|
||||
promo_group_id: number | null;
|
||||
promo_group_name: string | null;
|
||||
tariffs: UserAvailableTariff[];
|
||||
total: number;
|
||||
current_tariff_id: number | null;
|
||||
current_tariff_name: string | null;
|
||||
}
|
||||
|
||||
// Sync types
|
||||
export interface PanelUserInfo {
|
||||
uuid: string | null
|
||||
short_uuid: string | null
|
||||
username: string | null
|
||||
status: string | null
|
||||
expire_at: string | null
|
||||
traffic_limit_gb: number
|
||||
traffic_used_gb: number
|
||||
device_limit: number
|
||||
subscription_url: string | null
|
||||
active_squads: string[]
|
||||
uuid: string | null;
|
||||
short_uuid: string | null;
|
||||
username: string | null;
|
||||
status: string | null;
|
||||
expire_at: string | null;
|
||||
traffic_limit_gb: number;
|
||||
traffic_used_gb: number;
|
||||
device_limit: number;
|
||||
subscription_url: string | null;
|
||||
active_squads: string[];
|
||||
}
|
||||
|
||||
export interface SyncFromPanelResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
panel_user: PanelUserInfo | null
|
||||
changes: Record<string, unknown>
|
||||
errors: string[]
|
||||
success: boolean;
|
||||
message: string;
|
||||
panel_user: PanelUserInfo | null;
|
||||
changes: Record<string, unknown>;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface SyncToPanelResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
action: string
|
||||
panel_uuid: string | null
|
||||
changes: Record<string, unknown>
|
||||
errors: string[]
|
||||
success: boolean;
|
||||
message: string;
|
||||
action: string;
|
||||
panel_uuid: string | null;
|
||||
changes: Record<string, unknown>;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface PanelSyncStatusResponse {
|
||||
user_id: number
|
||||
telegram_id: number
|
||||
remnawave_uuid: string | null
|
||||
last_sync: string | null
|
||||
bot_subscription_status: string | null
|
||||
bot_subscription_end_date: string | null
|
||||
bot_traffic_limit_gb: number
|
||||
bot_traffic_used_gb: number
|
||||
bot_device_limit: number
|
||||
bot_squads: string[]
|
||||
panel_found: boolean
|
||||
panel_status: string | null
|
||||
panel_expire_at: string | null
|
||||
panel_traffic_limit_gb: number
|
||||
panel_traffic_used_gb: number
|
||||
panel_device_limit: number
|
||||
panel_squads: string[]
|
||||
has_differences: boolean
|
||||
differences: string[]
|
||||
user_id: number;
|
||||
telegram_id: number;
|
||||
remnawave_uuid: string | null;
|
||||
last_sync: string | null;
|
||||
bot_subscription_status: string | null;
|
||||
bot_subscription_end_date: string | null;
|
||||
bot_traffic_limit_gb: number;
|
||||
bot_traffic_used_gb: number;
|
||||
bot_device_limit: number;
|
||||
bot_squads: string[];
|
||||
panel_found: boolean;
|
||||
panel_status: string | null;
|
||||
panel_expire_at: string | null;
|
||||
panel_traffic_limit_gb: number;
|
||||
panel_traffic_used_gb: number;
|
||||
panel_device_limit: number;
|
||||
panel_squads: string[];
|
||||
has_differences: boolean;
|
||||
differences: string[];
|
||||
}
|
||||
|
||||
// Update types
|
||||
export interface UpdateBalanceRequest {
|
||||
amount_kopeks: number
|
||||
description?: string
|
||||
create_transaction?: boolean
|
||||
amount_kopeks: number;
|
||||
description?: string;
|
||||
create_transaction?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateBalanceResponse {
|
||||
success: boolean
|
||||
old_balance_kopeks: number
|
||||
new_balance_kopeks: number
|
||||
message: string
|
||||
success: boolean;
|
||||
old_balance_kopeks: number;
|
||||
new_balance_kopeks: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionRequest {
|
||||
action: 'extend' | 'set_end_date' | 'change_tariff' | 'set_traffic' | 'toggle_autopay' | 'cancel' | 'activate' | 'create'
|
||||
days?: number
|
||||
end_date?: string
|
||||
tariff_id?: number
|
||||
traffic_limit_gb?: number
|
||||
traffic_used_gb?: number
|
||||
autopay_enabled?: boolean
|
||||
is_trial?: boolean
|
||||
device_limit?: number
|
||||
action:
|
||||
| 'extend'
|
||||
| 'set_end_date'
|
||||
| 'change_tariff'
|
||||
| 'set_traffic'
|
||||
| 'toggle_autopay'
|
||||
| 'cancel'
|
||||
| 'activate'
|
||||
| 'create';
|
||||
days?: number;
|
||||
end_date?: string;
|
||||
tariff_id?: number;
|
||||
traffic_limit_gb?: number;
|
||||
traffic_used_gb?: number;
|
||||
autopay_enabled?: boolean;
|
||||
is_trial?: boolean;
|
||||
device_limit?: number;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
subscription: UserSubscriptionInfo | null
|
||||
success: boolean;
|
||||
message: string;
|
||||
subscription: UserSubscriptionInfo | null;
|
||||
}
|
||||
|
||||
export interface UpdateUserStatusResponse {
|
||||
success: boolean
|
||||
old_status: string
|
||||
new_status: string
|
||||
message: string
|
||||
success: boolean;
|
||||
old_status: string;
|
||||
new_status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UpdateRestrictionsRequest {
|
||||
restriction_topup?: boolean
|
||||
restriction_subscription?: boolean
|
||||
restriction_reason?: string
|
||||
restriction_topup?: boolean;
|
||||
restriction_subscription?: boolean;
|
||||
restriction_reason?: string;
|
||||
}
|
||||
|
||||
export interface UpdateRestrictionsResponse {
|
||||
success: boolean
|
||||
restriction_topup: boolean
|
||||
restriction_subscription: boolean
|
||||
restriction_reason: string | null
|
||||
message: string
|
||||
success: boolean;
|
||||
restriction_topup: boolean;
|
||||
restriction_subscription: boolean;
|
||||
restriction_reason: string | null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SyncFromPanelRequest {
|
||||
update_subscription?: boolean
|
||||
update_traffic?: boolean
|
||||
create_if_missing?: boolean
|
||||
update_subscription?: boolean;
|
||||
update_traffic?: boolean;
|
||||
create_if_missing?: boolean;
|
||||
}
|
||||
|
||||
export interface SyncToPanelRequest {
|
||||
create_if_missing?: boolean
|
||||
update_status?: boolean
|
||||
update_traffic_limit?: boolean
|
||||
update_expire_date?: boolean
|
||||
update_squads?: boolean
|
||||
create_if_missing?: boolean;
|
||||
update_status?: boolean;
|
||||
update_traffic_limit?: boolean;
|
||||
update_expire_date?: boolean;
|
||||
update_squads?: boolean;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
|
||||
export const adminUsersApi = {
|
||||
// List users
|
||||
getUsers: async (params: {
|
||||
offset?: number
|
||||
limit?: number
|
||||
search?: string
|
||||
status?: 'active' | 'blocked' | 'deleted'
|
||||
sort_by?: 'created_at' | 'balance' | 'traffic' | 'last_activity' | 'total_spent' | 'purchase_count'
|
||||
} = {}): Promise<UsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/users', { params })
|
||||
return response.data
|
||||
getUsers: async (
|
||||
params: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
status?: 'active' | 'blocked' | 'deleted';
|
||||
sort_by?:
|
||||
| 'created_at'
|
||||
| 'balance'
|
||||
| 'traffic'
|
||||
| 'last_activity'
|
||||
| 'total_spent'
|
||||
| 'purchase_count';
|
||||
} = {},
|
||||
): Promise<UsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/users', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get users stats
|
||||
getStats: async (): Promise<UsersStatsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/users/stats')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/users/stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get user detail
|
||||
getUser: async (userId: number): Promise<UserDetailResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get user by telegram ID
|
||||
getUserByTelegram: async (telegramId: number): Promise<UserDetailResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/by-telegram/${telegramId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/users/by-telegram/${telegramId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available tariffs for user
|
||||
getAvailableTariffs: async (userId: number, includeInactive = false): Promise<UserAvailableTariffsResponse> => {
|
||||
getAvailableTariffs: async (
|
||||
userId: number,
|
||||
includeInactive = false,
|
||||
): Promise<UserAvailableTariffsResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/available-tariffs`, {
|
||||
params: { include_inactive: includeInactive }
|
||||
})
|
||||
return response.data
|
||||
params: { include_inactive: includeInactive },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update balance
|
||||
updateBalance: async (userId: number, data: UpdateBalanceRequest): Promise<UpdateBalanceResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/balance`, data)
|
||||
return response.data
|
||||
updateBalance: async (
|
||||
userId: number,
|
||||
data: UpdateBalanceRequest,
|
||||
): Promise<UpdateBalanceResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/balance`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update subscription
|
||||
updateSubscription: async (userId: number, data: UpdateSubscriptionRequest): Promise<UpdateSubscriptionResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/subscription`, data)
|
||||
return response.data
|
||||
updateSubscription: async (
|
||||
userId: number,
|
||||
data: UpdateSubscriptionRequest,
|
||||
): Promise<UpdateSubscriptionResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/subscription`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update status
|
||||
updateStatus: async (userId: number, status: 'active' | 'blocked' | 'deleted', reason?: string): Promise<UpdateUserStatusResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/status`, { status, reason })
|
||||
return response.data
|
||||
updateStatus: async (
|
||||
userId: number,
|
||||
status: 'active' | 'blocked' | 'deleted',
|
||||
reason?: string,
|
||||
): Promise<UpdateUserStatusResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/status`, {
|
||||
status,
|
||||
reason,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Block user
|
||||
blockUser: async (userId: number, reason?: string): Promise<UpdateUserStatusResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/block`, null, { params: { reason } })
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/block`, null, {
|
||||
params: { reason },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Unblock user
|
||||
unblockUser: async (userId: number): Promise<UpdateUserStatusResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/unblock`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/unblock`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update restrictions
|
||||
updateRestrictions: async (userId: number, data: UpdateRestrictionsRequest): Promise<UpdateRestrictionsResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/restrictions`, data)
|
||||
return response.data
|
||||
updateRestrictions: async (
|
||||
userId: number,
|
||||
data: UpdateRestrictionsRequest,
|
||||
): Promise<UpdateRestrictionsResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/restrictions`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update promo group
|
||||
updatePromoGroup: async (userId: number, promoGroupId: number | null): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/promo-group`, { promo_group_id: promoGroupId })
|
||||
return response.data
|
||||
updatePromoGroup: async (
|
||||
userId: number,
|
||||
promoGroupId: number | null,
|
||||
): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/promo-group`, {
|
||||
promo_group_id: promoGroupId,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete user
|
||||
deleteUser: async (userId: number, softDelete = true, reason?: string): Promise<{ success: boolean; message: string }> => {
|
||||
deleteUser: async (
|
||||
userId: number,
|
||||
softDelete = true,
|
||||
reason?: string,
|
||||
): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.delete(`/cabinet/admin/users/${userId}`, {
|
||||
data: { soft_delete: softDelete, reason }
|
||||
})
|
||||
return response.data
|
||||
data: { soft_delete: softDelete, reason },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get referrals
|
||||
getReferrals: async (userId: number, offset = 0, limit = 50): Promise<UsersListResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, {
|
||||
params: { offset, limit }
|
||||
})
|
||||
return response.data
|
||||
params: { offset, limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get transactions
|
||||
getTransactions: async (userId: number, params: {
|
||||
offset?: number
|
||||
limit?: number
|
||||
transaction_type?: string
|
||||
} = {}): Promise<{ transactions: UserTransactionItem[]; total: number; offset: number; limit: number }> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/transactions`, { params })
|
||||
return response.data
|
||||
getTransactions: async (
|
||||
userId: number,
|
||||
params: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
transaction_type?: string;
|
||||
} = {},
|
||||
): Promise<{
|
||||
transactions: UserTransactionItem[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/transactions`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Sync status
|
||||
getSyncStatus: async (userId: number): Promise<PanelSyncStatusResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Sync from panel
|
||||
syncFromPanel: async (userId: number, data: SyncFromPanelRequest = {}): Promise<SyncFromPanelResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data)
|
||||
return response.data
|
||||
syncFromPanel: async (
|
||||
userId: number,
|
||||
data: SyncFromPanelRequest = {},
|
||||
): Promise<SyncFromPanelResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Sync to panel
|
||||
syncToPanel: async (userId: number, data: SyncToPanelRequest = {}): Promise<SyncToPanelResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data)
|
||||
return response.data
|
||||
syncToPanel: async (
|
||||
userId: number,
|
||||
data: SyncToPanelRequest = {},
|
||||
): Promise<SyncToPanelResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import apiClient from './client'
|
||||
import type { AuthResponse, RegisterResponse, TokenResponse, User } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { AuthResponse, RegisterResponse, TokenResponse, User } from '../types';
|
||||
|
||||
export const authApi = {
|
||||
// Telegram WebApp authentication
|
||||
loginTelegram: async (initData: string): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram', {
|
||||
init_data: initData,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Telegram Login Widget authentication
|
||||
loginTelegramWidget: async (data: {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name?: string
|
||||
username?: string
|
||||
photo_url?: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', data)
|
||||
return response.data
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Email login
|
||||
@@ -29,63 +29,69 @@ export const authApi = {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/login', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register email (link to existing Telegram account)
|
||||
registerEmail: async (email: string, password: string): Promise<{ message: string; email: string }> => {
|
||||
registerEmail: async (
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<{ message: string; email: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/email/register', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register standalone email account (no Telegram required)
|
||||
// Returns message - user must verify email before login
|
||||
registerEmailStandalone: async (data: {
|
||||
email: string
|
||||
password: string
|
||||
first_name?: string
|
||||
language?: string
|
||||
referral_code?: string
|
||||
email: string;
|
||||
password: string;
|
||||
first_name?: string;
|
||||
language?: string;
|
||||
referral_code?: string;
|
||||
}): Promise<RegisterResponse> => {
|
||||
const response = await apiClient.post<RegisterResponse>('/cabinet/auth/email/register/standalone', data)
|
||||
return response.data
|
||||
const response = await apiClient.post<RegisterResponse>(
|
||||
'/cabinet/auth/email/register/standalone',
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Verify email and get auth tokens
|
||||
verifyEmail: async (token: string): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', { token })
|
||||
return response.data
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', { token });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Resend verification email
|
||||
resendVerification: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/email/resend')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/auth/email/resend');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Refresh token
|
||||
refreshToken: async (refreshToken: string): Promise<TokenResponse> => {
|
||||
const response = await apiClient.post<TokenResponse>('/cabinet/auth/refresh', {
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout: async (refreshToken: string): Promise<void> => {
|
||||
await apiClient.post('/cabinet/auth/logout', {
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
// Forgot password
|
||||
forgotPassword: async (email: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/password/forgot', { email })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/auth/password/forgot', { email });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reset password
|
||||
@@ -93,13 +99,13 @@ export const authApi = {
|
||||
const response = await apiClient.post('/cabinet/auth/password/reset', {
|
||||
token,
|
||||
password,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get current user
|
||||
getMe: async (): Promise<User> => {
|
||||
const response = await apiClient.get<User>('/cabinet/auth/me')
|
||||
return response.data
|
||||
const response = await apiClient.get<User>('/cabinet/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,100 +1,124 @@
|
||||
import apiClient from './client'
|
||||
import type { Balance, Transaction, PaymentMethod, PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
|
||||
import apiClient from './client';
|
||||
import type {
|
||||
Balance,
|
||||
Transaction,
|
||||
PaymentMethod,
|
||||
PaginatedResponse,
|
||||
PendingPayment,
|
||||
ManualCheckResponse,
|
||||
} from '../types';
|
||||
|
||||
export const balanceApi = {
|
||||
// Get current balance
|
||||
getBalance: async (): Promise<Balance> => {
|
||||
const response = await apiClient.get<Balance>('/cabinet/balance')
|
||||
return response.data
|
||||
const response = await apiClient.get<Balance>('/cabinet/balance');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get transaction history
|
||||
getTransactions: async (params?: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
type?: string
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
type?: string;
|
||||
}): Promise<PaginatedResponse<Transaction>> => {
|
||||
const response = await apiClient.get<PaginatedResponse<Transaction>>('/cabinet/balance/transactions', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.get<PaginatedResponse<Transaction>>(
|
||||
'/cabinet/balance/transactions',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available payment methods
|
||||
getPaymentMethods: async (): Promise<PaymentMethod[]> => {
|
||||
const response = await apiClient.get<PaymentMethod[]>('/cabinet/balance/payment-methods')
|
||||
return response.data
|
||||
const response = await apiClient.get<PaymentMethod[]>('/cabinet/balance/payment-methods');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create top-up payment
|
||||
createTopUp: async (amountKopeks: number, paymentMethod: string, paymentOption?: string): Promise<{
|
||||
payment_id: string
|
||||
payment_url: string
|
||||
amount_kopeks: number
|
||||
amount_rubles: number
|
||||
status: string
|
||||
expires_at: string | null
|
||||
createTopUp: async (
|
||||
amountKopeks: number,
|
||||
paymentMethod: string,
|
||||
paymentOption?: string,
|
||||
): Promise<{
|
||||
payment_id: string;
|
||||
payment_url: string;
|
||||
amount_kopeks: number;
|
||||
amount_rubles: number;
|
||||
status: string;
|
||||
expires_at: string | null;
|
||||
}> => {
|
||||
const payload: {
|
||||
amount_kopeks: number
|
||||
payment_method: string
|
||||
payment_option?: string
|
||||
amount_kopeks: number;
|
||||
payment_method: string;
|
||||
payment_option?: string;
|
||||
} = {
|
||||
amount_kopeks: amountKopeks,
|
||||
payment_method: paymentMethod,
|
||||
}
|
||||
};
|
||||
if (paymentOption) {
|
||||
payload.payment_option = paymentOption
|
||||
payload.payment_option = paymentOption;
|
||||
}
|
||||
const response = await apiClient.post('/cabinet/balance/topup', payload)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/balance/topup', payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Activate promo code
|
||||
activatePromocode: async (code: string): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
balance_before: number
|
||||
balance_after: number
|
||||
bonus_description: string | null
|
||||
activatePromocode: async (
|
||||
code: string,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
balance_before: number;
|
||||
balance_after: number;
|
||||
bonus_description: string | null;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/promocode/activate', { code })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/promocode/activate', { code });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create Telegram Stars invoice for Mini App balance top-up
|
||||
createStarsInvoice: async (amountKopeks: number): Promise<{
|
||||
invoice_url: string
|
||||
stars_amount?: number
|
||||
amount_kopeks?: number
|
||||
createStarsInvoice: async (
|
||||
amountKopeks: number,
|
||||
): Promise<{
|
||||
invoice_url: string;
|
||||
stars_amount?: number;
|
||||
amount_kopeks?: number;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/balance/stars-invoice', {
|
||||
amount_kopeks: amountKopeks,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get pending payments for manual verification
|
||||
getPendingPayments: async (params?: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}): Promise<PaginatedResponse<PendingPayment>> => {
|
||||
const response = await apiClient.get<PaginatedResponse<PendingPayment>>('/cabinet/balance/pending-payments', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
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
|
||||
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
|
||||
const response = await apiClient.post<ManualCheckResponse>(
|
||||
`/cabinet/balance/pending-payments/${method}/${paymentId}/check`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,270 +1,270 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface BanSystemStatus {
|
||||
enabled: boolean
|
||||
configured: boolean
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
}
|
||||
|
||||
export interface BanSystemStats {
|
||||
total_users: number
|
||||
active_users: number
|
||||
users_over_limit: number
|
||||
total_requests: number
|
||||
total_punishments: number
|
||||
active_punishments: number
|
||||
nodes_online: number
|
||||
nodes_total: number
|
||||
agents_online: number
|
||||
agents_total: number
|
||||
panel_connected: boolean
|
||||
uptime_seconds: number | null
|
||||
total_users: number;
|
||||
active_users: number;
|
||||
users_over_limit: number;
|
||||
total_requests: number;
|
||||
total_punishments: number;
|
||||
active_punishments: number;
|
||||
nodes_online: number;
|
||||
nodes_total: number;
|
||||
agents_online: number;
|
||||
agents_total: number;
|
||||
panel_connected: boolean;
|
||||
uptime_seconds: number | null;
|
||||
}
|
||||
|
||||
export interface BanUserIPInfo {
|
||||
ip: string
|
||||
first_seen: string | null
|
||||
last_seen: string | null
|
||||
node: string | null
|
||||
request_count: number
|
||||
country_code: string | null
|
||||
country_name: string | null
|
||||
city: string | null
|
||||
ip: string;
|
||||
first_seen: string | null;
|
||||
last_seen: string | null;
|
||||
node: string | null;
|
||||
request_count: number;
|
||||
country_code: string | null;
|
||||
country_name: string | null;
|
||||
city: string | null;
|
||||
}
|
||||
|
||||
export interface BanUserRequestLog {
|
||||
timestamp: string
|
||||
source_ip: string
|
||||
destination: string | null
|
||||
dest_port: number | null
|
||||
protocol: string | null
|
||||
action: string | null
|
||||
node: string | null
|
||||
timestamp: string;
|
||||
source_ip: string;
|
||||
destination: string | null;
|
||||
dest_port: number | null;
|
||||
protocol: string | null;
|
||||
action: string | null;
|
||||
node: string | null;
|
||||
}
|
||||
|
||||
export interface BanUserListItem {
|
||||
email: string
|
||||
unique_ip_count: number
|
||||
total_requests: number
|
||||
limit: number | null
|
||||
is_over_limit: boolean
|
||||
blocked_count: number
|
||||
last_seen: string | null
|
||||
email: string;
|
||||
unique_ip_count: number;
|
||||
total_requests: number;
|
||||
limit: number | null;
|
||||
is_over_limit: boolean;
|
||||
blocked_count: number;
|
||||
last_seen: string | null;
|
||||
}
|
||||
|
||||
export interface BanUsersListResponse {
|
||||
users: BanUserListItem[]
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
users: BanUserListItem[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface BanUserDetailResponse {
|
||||
email: string
|
||||
unique_ip_count: number
|
||||
total_requests: number
|
||||
limit: number | null
|
||||
is_over_limit: boolean
|
||||
blocked_count: number
|
||||
ips: BanUserIPInfo[]
|
||||
recent_requests: BanUserRequestLog[]
|
||||
network_type: string | null
|
||||
email: string;
|
||||
unique_ip_count: number;
|
||||
total_requests: number;
|
||||
limit: number | null;
|
||||
is_over_limit: boolean;
|
||||
blocked_count: number;
|
||||
ips: BanUserIPInfo[];
|
||||
recent_requests: BanUserRequestLog[];
|
||||
network_type: string | null;
|
||||
}
|
||||
|
||||
export interface BanPunishmentItem {
|
||||
id: number | null
|
||||
user_id: string
|
||||
uuid: string | null
|
||||
username: string
|
||||
reason: string | null
|
||||
punished_at: string
|
||||
enable_at: string | null
|
||||
ip_count: number
|
||||
limit: number
|
||||
enabled: boolean
|
||||
enabled_at: string | null
|
||||
node_name: string | null
|
||||
id: number | null;
|
||||
user_id: string;
|
||||
uuid: string | null;
|
||||
username: string;
|
||||
reason: string | null;
|
||||
punished_at: string;
|
||||
enable_at: string | null;
|
||||
ip_count: number;
|
||||
limit: number;
|
||||
enabled: boolean;
|
||||
enabled_at: string | null;
|
||||
node_name: string | null;
|
||||
}
|
||||
|
||||
export interface BanPunishmentsListResponse {
|
||||
punishments: BanPunishmentItem[]
|
||||
total: number
|
||||
punishments: BanPunishmentItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BanHistoryResponse {
|
||||
items: BanPunishmentItem[]
|
||||
total: number
|
||||
items: BanPunishmentItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BanUserRequest {
|
||||
username: string
|
||||
minutes: number
|
||||
reason?: string
|
||||
username: string;
|
||||
minutes: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface UnbanResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BanNodeItem {
|
||||
name: string
|
||||
address: string | null
|
||||
is_connected: boolean
|
||||
last_seen: string | null
|
||||
users_count: number
|
||||
agent_stats: Record<string, unknown> | null
|
||||
name: string;
|
||||
address: string | null;
|
||||
is_connected: boolean;
|
||||
last_seen: string | null;
|
||||
users_count: number;
|
||||
agent_stats: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface BanNodesListResponse {
|
||||
nodes: BanNodeItem[]
|
||||
total: number
|
||||
online: number
|
||||
nodes: BanNodeItem[];
|
||||
total: number;
|
||||
online: number;
|
||||
}
|
||||
|
||||
export interface BanAgentItem {
|
||||
node_name: string
|
||||
sent_total: number
|
||||
dropped_total: number
|
||||
batches_total: number
|
||||
reconnects: number
|
||||
failures: number
|
||||
queue_size: number
|
||||
queue_max: number
|
||||
dedup_checked: number
|
||||
dedup_skipped: number
|
||||
filter_checked: number
|
||||
filter_filtered: number
|
||||
health: string
|
||||
is_online: boolean
|
||||
last_report: string | null
|
||||
node_name: string;
|
||||
sent_total: number;
|
||||
dropped_total: number;
|
||||
batches_total: number;
|
||||
reconnects: number;
|
||||
failures: number;
|
||||
queue_size: number;
|
||||
queue_max: number;
|
||||
dedup_checked: number;
|
||||
dedup_skipped: number;
|
||||
filter_checked: number;
|
||||
filter_filtered: number;
|
||||
health: string;
|
||||
is_online: boolean;
|
||||
last_report: string | null;
|
||||
}
|
||||
|
||||
export interface BanAgentsSummary {
|
||||
total_agents: number
|
||||
online_agents: number
|
||||
total_sent: number
|
||||
total_dropped: number
|
||||
avg_queue_size: number
|
||||
healthy_count: number
|
||||
warning_count: number
|
||||
critical_count: number
|
||||
total_agents: number;
|
||||
online_agents: number;
|
||||
total_sent: number;
|
||||
total_dropped: number;
|
||||
avg_queue_size: number;
|
||||
healthy_count: number;
|
||||
warning_count: number;
|
||||
critical_count: number;
|
||||
}
|
||||
|
||||
export interface BanAgentsListResponse {
|
||||
agents: BanAgentItem[]
|
||||
summary: BanAgentsSummary | null
|
||||
total: number
|
||||
online: number
|
||||
agents: BanAgentItem[];
|
||||
summary: BanAgentsSummary | null;
|
||||
total: number;
|
||||
online: number;
|
||||
}
|
||||
|
||||
export interface BanTrafficViolationItem {
|
||||
id: number | null
|
||||
username: string
|
||||
email: string | null
|
||||
violation_type: string
|
||||
description: string | null
|
||||
bytes_used: number
|
||||
bytes_limit: number
|
||||
detected_at: string
|
||||
resolved: boolean
|
||||
id: number | null;
|
||||
username: string;
|
||||
email: string | null;
|
||||
violation_type: string;
|
||||
description: string | null;
|
||||
bytes_used: number;
|
||||
bytes_limit: number;
|
||||
detected_at: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
export interface BanTrafficViolationsResponse {
|
||||
violations: BanTrafficViolationItem[]
|
||||
total: number
|
||||
violations: BanTrafficViolationItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BanTrafficTopItem {
|
||||
username: string
|
||||
bytes_total: number
|
||||
bytes_limit: number | null
|
||||
over_limit: boolean
|
||||
username: string;
|
||||
bytes_total: number;
|
||||
bytes_limit: number | null;
|
||||
over_limit: boolean;
|
||||
}
|
||||
|
||||
export interface BanTrafficResponse {
|
||||
enabled: boolean
|
||||
stats: Record<string, unknown> | null
|
||||
top_users: BanTrafficTopItem[]
|
||||
recent_violations: BanTrafficViolationItem[]
|
||||
enabled: boolean;
|
||||
stats: Record<string, unknown> | null;
|
||||
top_users: BanTrafficTopItem[];
|
||||
recent_violations: BanTrafficViolationItem[];
|
||||
}
|
||||
|
||||
// === Settings Types ===
|
||||
|
||||
export interface BanSettingDefinition {
|
||||
key: string
|
||||
value: unknown
|
||||
type: string
|
||||
min_value: number | null
|
||||
max_value: number | null
|
||||
editable: boolean
|
||||
description: string | null
|
||||
category: string | null
|
||||
key: string;
|
||||
value: unknown;
|
||||
type: string;
|
||||
min_value: number | null;
|
||||
max_value: number | null;
|
||||
editable: boolean;
|
||||
description: string | null;
|
||||
category: string | null;
|
||||
}
|
||||
|
||||
export interface BanSettingsResponse {
|
||||
settings: BanSettingDefinition[]
|
||||
settings: BanSettingDefinition[];
|
||||
}
|
||||
|
||||
export interface BanWhitelistRequest {
|
||||
username: string
|
||||
username: string;
|
||||
}
|
||||
|
||||
// === Report Types ===
|
||||
|
||||
export interface BanReportTopViolator {
|
||||
username: string
|
||||
count: number
|
||||
username: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface BanReportResponse {
|
||||
period_hours: number
|
||||
current_users: number
|
||||
current_ips: number
|
||||
punishment_stats: Record<string, unknown> | null
|
||||
top_violators: BanReportTopViolator[]
|
||||
period_hours: number;
|
||||
current_users: number;
|
||||
current_ips: number;
|
||||
punishment_stats: Record<string, unknown> | null;
|
||||
top_violators: BanReportTopViolator[];
|
||||
}
|
||||
|
||||
// === Health Types ===
|
||||
|
||||
export interface BanHealthComponent {
|
||||
name: string
|
||||
status: string
|
||||
message: string | null
|
||||
details: Record<string, unknown> | null
|
||||
name: string;
|
||||
status: string;
|
||||
message: string | null;
|
||||
details: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface BanHealthResponse {
|
||||
status: string
|
||||
uptime: number | null
|
||||
components: BanHealthComponent[]
|
||||
status: string;
|
||||
uptime: number | null;
|
||||
components: BanHealthComponent[];
|
||||
}
|
||||
|
||||
export interface BanHealthDetailedResponse {
|
||||
status: string
|
||||
uptime: number | null
|
||||
components: Record<string, unknown>
|
||||
status: string;
|
||||
uptime: number | null;
|
||||
components: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// === Agent History Types ===
|
||||
|
||||
export interface BanAgentHistoryItem {
|
||||
timestamp: string
|
||||
sent_total: number
|
||||
dropped_total: number
|
||||
queue_size: number
|
||||
batches_total: number
|
||||
timestamp: string;
|
||||
sent_total: number;
|
||||
dropped_total: number;
|
||||
queue_size: number;
|
||||
batches_total: number;
|
||||
}
|
||||
|
||||
export interface BanAgentHistoryResponse {
|
||||
node: string
|
||||
hours: number
|
||||
records: number
|
||||
delta: Record<string, unknown> | null
|
||||
first: Record<string, unknown> | null
|
||||
last: Record<string, unknown> | null
|
||||
history: BanAgentHistoryItem[]
|
||||
node: string;
|
||||
hours: number;
|
||||
records: number;
|
||||
delta: Record<string, unknown> | null;
|
||||
first: Record<string, unknown> | null;
|
||||
last: Record<string, unknown> | null;
|
||||
history: BanAgentHistoryItem[];
|
||||
}
|
||||
|
||||
// === API ===
|
||||
@@ -272,174 +272,198 @@ export interface BanAgentHistoryResponse {
|
||||
export const banSystemApi = {
|
||||
// Status
|
||||
getStatus: async (): Promise<BanSystemStatus> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/status')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Stats
|
||||
getStats: async (): Promise<BanSystemStats> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/stats')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Users
|
||||
getUsers: async (params: {
|
||||
offset?: number
|
||||
limit?: number
|
||||
status?: string
|
||||
} = {}): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params })
|
||||
return response.data
|
||||
getUsers: async (
|
||||
params: {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
status?: string;
|
||||
} = {},
|
||||
): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUsersOverLimit: async (limit: number = 50): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/users/over-limit', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
searchUsers: async (query: string): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(
|
||||
`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUser: async (email: string): Promise<BanUserDetailResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(
|
||||
`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Punishments
|
||||
getPunishments: async (): Promise<BanPunishmentsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/punishments')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/punishments');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
unbanUser: async (userId: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
banUser: async (data: BanUserRequest): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/ban', data)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/ban', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPunishmentHistory: async (query: string, limit: number = 20): Promise<BanHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`, {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.get(
|
||||
`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`,
|
||||
{
|
||||
params: { limit },
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Nodes
|
||||
getNodes: async (): Promise<BanNodesListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/nodes')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/nodes');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Agents
|
||||
getAgents: async (params: {
|
||||
search?: string
|
||||
health?: string
|
||||
status?: string
|
||||
} = {}): Promise<BanAgentsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params })
|
||||
return response.data
|
||||
getAgents: async (
|
||||
params: {
|
||||
search?: string;
|
||||
health?: string;
|
||||
status?: string;
|
||||
} = {},
|
||||
): Promise<BanAgentsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getAgentsSummary: async (): Promise<BanAgentsSummary> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Traffic violations
|
||||
getTrafficViolations: async (limit: number = 50): Promise<BanTrafficViolationsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/violations', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Full Traffic
|
||||
getTraffic: async (): Promise<BanTrafficResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTrafficTop: async (limit: number = 20): Promise<BanTrafficTopItem[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/top', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
params: { limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Settings
|
||||
getSettings: async (): Promise<BanSettingsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/settings')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/settings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getSetting: async (key: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
setSetting: async (key: string, value: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}`, null, {
|
||||
params: { value }
|
||||
})
|
||||
return response.data
|
||||
params: { value },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
toggleSetting: async (key: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Whitelist
|
||||
whitelistAdd: async (username: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', { username })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', {
|
||||
username,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
whitelistRemove: async (username: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', { username })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', {
|
||||
username,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reports
|
||||
getReport: async (hours: number = 24): Promise<BanReportResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/report', {
|
||||
params: { hours }
|
||||
})
|
||||
return response.data
|
||||
params: { hours },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Health
|
||||
getHealth: async (): Promise<BanHealthResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getHealthDetailed: async (): Promise<BanHealthDetailedResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Agent History
|
||||
getAgentHistory: async (nodeName: string, hours: number = 24): Promise<BanAgentHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`, {
|
||||
params: { hours }
|
||||
})
|
||||
return response.data
|
||||
getAgentHistory: async (
|
||||
nodeName: string,
|
||||
hours: number = 24,
|
||||
): Promise<BanAgentHistoryResponse> => {
|
||||
const response = await apiClient.get(
|
||||
`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`,
|
||||
{
|
||||
params: { hours },
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// User Punishment History
|
||||
getUserHistory: async (email: string, limit: number = 20): Promise<BanHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`, {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.get(
|
||||
`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`,
|
||||
{
|
||||
params: { limit },
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,203 +1,209 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface BrandingInfo {
|
||||
name: string
|
||||
logo_url: string | null
|
||||
logo_letter: string
|
||||
has_custom_logo: boolean
|
||||
name: string;
|
||||
logo_url: string | null;
|
||||
logo_letter: string;
|
||||
has_custom_logo: boolean;
|
||||
}
|
||||
|
||||
export interface AnimationEnabled {
|
||||
enabled: boolean
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FullscreenEnabled {
|
||||
enabled: boolean
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface EmailAuthEnabled {
|
||||
enabled: boolean
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface AnalyticsCounters {
|
||||
yandex_metrika_id: string
|
||||
google_ads_id: string
|
||||
google_ads_label: string
|
||||
yandex_metrika_id: string;
|
||||
google_ads_id: string;
|
||||
google_ads_label: string;
|
||||
}
|
||||
|
||||
const BRANDING_CACHE_KEY = 'cabinet_branding'
|
||||
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded'
|
||||
const BRANDING_CACHE_KEY = 'cabinet_branding';
|
||||
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded';
|
||||
|
||||
// Check if logo was already preloaded in this session
|
||||
export const isLogoPreloaded = (): boolean => {
|
||||
try {
|
||||
const cached = getCachedBranding()
|
||||
const cached = getCachedBranding();
|
||||
if (!cached?.has_custom_logo || !cached?.logo_url) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`
|
||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY)
|
||||
return preloaded === logoUrl
|
||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`;
|
||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
||||
return preloaded === logoUrl;
|
||||
} catch {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get cached branding from localStorage
|
||||
export const getCachedBranding = (): BrandingInfo | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem(BRANDING_CACHE_KEY)
|
||||
const cached = localStorage.getItem(BRANDING_CACHE_KEY);
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
} catch {
|
||||
// localStorage not available or invalid JSON
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Update branding cache in localStorage
|
||||
export const setCachedBranding = (branding: BrandingInfo) => {
|
||||
try {
|
||||
localStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding))
|
||||
localStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding));
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Preload logo image for instant display
|
||||
export const preloadLogo = (branding: BrandingInfo): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!branding.has_custom_logo || !branding.logo_url) {
|
||||
resolve()
|
||||
return
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
|
||||
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
|
||||
|
||||
// Check if already preloaded in this session
|
||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY)
|
||||
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
|
||||
if (preloaded === logoUrl) {
|
||||
resolve()
|
||||
return
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image()
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl)
|
||||
resolve()
|
||||
}
|
||||
img.onerror = () => resolve()
|
||||
img.src = logoUrl
|
||||
})
|
||||
}
|
||||
sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => resolve();
|
||||
img.src = logoUrl;
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize logo preload from cache on page load
|
||||
export const initLogoPreload = () => {
|
||||
const cached = getCachedBranding()
|
||||
const cached = getCachedBranding();
|
||||
if (cached) {
|
||||
preloadLogo(cached)
|
||||
preloadLogo(cached);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const brandingApi = {
|
||||
// Get current branding (public, no auth required)
|
||||
getBranding: async (): Promise<BrandingInfo> => {
|
||||
const response = await apiClient.get<BrandingInfo>('/cabinet/branding')
|
||||
return response.data
|
||||
const response = await apiClient.get<BrandingInfo>('/cabinet/branding');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update project name (admin only)
|
||||
updateName: async (name: string): Promise<BrandingInfo> => {
|
||||
const response = await apiClient.put<BrandingInfo>('/cabinet/branding/name', { name })
|
||||
return response.data
|
||||
const response = await apiClient.put<BrandingInfo>('/cabinet/branding/name', { name });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Upload custom logo (admin only)
|
||||
uploadLogo: async (file: File): Promise<BrandingInfo> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await apiClient.post<BrandingInfo>('/cabinet/branding/logo', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete custom logo (admin only)
|
||||
deleteLogo: async (): Promise<BrandingInfo> => {
|
||||
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo')
|
||||
return response.data
|
||||
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get logo URL (without cache busting - server handles caching via Cache-Control headers)
|
||||
getLogoUrl: (branding: BrandingInfo): string | null => {
|
||||
if (!branding.has_custom_logo || !branding.logo_url) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
|
||||
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
|
||||
},
|
||||
|
||||
// Get animation enabled (public, no auth required)
|
||||
getAnimationEnabled: async (): Promise<AnimationEnabled> => {
|
||||
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation')
|
||||
return response.data
|
||||
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update animation enabled (admin only)
|
||||
updateAnimationEnabled: async (enabled: boolean): Promise<AnimationEnabled> => {
|
||||
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', { enabled })
|
||||
return response.data
|
||||
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get fullscreen enabled (public, no auth required)
|
||||
getFullscreenEnabled: async (): Promise<FullscreenEnabled> => {
|
||||
try {
|
||||
const response = await apiClient.get<FullscreenEnabled>('/cabinet/branding/fullscreen')
|
||||
return response.data
|
||||
const response = await apiClient.get<FullscreenEnabled>('/cabinet/branding/fullscreen');
|
||||
return response.data;
|
||||
} catch {
|
||||
// If endpoint doesn't exist, default to disabled
|
||||
return { enabled: false }
|
||||
return { enabled: false };
|
||||
}
|
||||
},
|
||||
|
||||
// Update fullscreen enabled (admin only)
|
||||
updateFullscreenEnabled: async (enabled: boolean): Promise<FullscreenEnabled> => {
|
||||
const response = await apiClient.patch<FullscreenEnabled>('/cabinet/branding/fullscreen', { enabled })
|
||||
return response.data
|
||||
const response = await apiClient.patch<FullscreenEnabled>('/cabinet/branding/fullscreen', {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get email auth enabled (public, no auth required)
|
||||
getEmailAuthEnabled: async (): Promise<EmailAuthEnabled> => {
|
||||
try {
|
||||
const response = await apiClient.get<EmailAuthEnabled>('/cabinet/branding/email-auth')
|
||||
return response.data
|
||||
const response = await apiClient.get<EmailAuthEnabled>('/cabinet/branding/email-auth');
|
||||
return response.data;
|
||||
} catch {
|
||||
// If endpoint doesn't exist, default to enabled
|
||||
return { enabled: true }
|
||||
return { enabled: true };
|
||||
}
|
||||
},
|
||||
|
||||
// Update email auth enabled (admin only)
|
||||
updateEmailAuthEnabled: async (enabled: boolean): Promise<EmailAuthEnabled> => {
|
||||
const response = await apiClient.patch<EmailAuthEnabled>('/cabinet/branding/email-auth', { enabled })
|
||||
return response.data
|
||||
const response = await apiClient.patch<EmailAuthEnabled>('/cabinet/branding/email-auth', {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get analytics counters (public, no auth required)
|
||||
getAnalyticsCounters: async (): Promise<AnalyticsCounters> => {
|
||||
try {
|
||||
const response = await apiClient.get<AnalyticsCounters>('/cabinet/branding/analytics')
|
||||
return response.data
|
||||
const response = await apiClient.get<AnalyticsCounters>('/cabinet/branding/analytics');
|
||||
return response.data;
|
||||
} catch {
|
||||
return { yandex_metrika_id: '', google_ads_id: '', google_ads_label: '' }
|
||||
return { yandex_metrika_id: '', google_ads_id: '', google_ads_label: '' };
|
||||
}
|
||||
},
|
||||
|
||||
// Update analytics counters (admin only)
|
||||
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
|
||||
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data)
|
||||
return response.data
|
||||
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,230 +1,241 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// Types
|
||||
export type CampaignBonusType = 'balance' | 'subscription' | 'none' | 'tariff'
|
||||
export type CampaignBonusType = 'balance' | 'subscription' | 'none' | 'tariff';
|
||||
|
||||
export interface TariffInfo {
|
||||
id: number
|
||||
name: string
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CampaignListItem {
|
||||
id: number
|
||||
name: string
|
||||
start_parameter: string
|
||||
bonus_type: CampaignBonusType
|
||||
is_active: boolean
|
||||
registrations_count: number
|
||||
total_revenue_kopeks: number
|
||||
conversion_rate: number
|
||||
created_at: string
|
||||
id: number;
|
||||
name: string;
|
||||
start_parameter: string;
|
||||
bonus_type: CampaignBonusType;
|
||||
is_active: boolean;
|
||||
registrations_count: number;
|
||||
total_revenue_kopeks: number;
|
||||
conversion_rate: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CampaignListResponse {
|
||||
campaigns: CampaignListItem[]
|
||||
total: number
|
||||
campaigns: CampaignListItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CampaignDetail {
|
||||
id: number
|
||||
name: string
|
||||
start_parameter: string
|
||||
bonus_type: CampaignBonusType
|
||||
is_active: boolean
|
||||
balance_bonus_kopeks: number
|
||||
balance_bonus_rubles: number
|
||||
subscription_duration_days: number | null
|
||||
subscription_traffic_gb: number | null
|
||||
subscription_device_limit: number | null
|
||||
subscription_squads: string[]
|
||||
tariff_id: number | null
|
||||
tariff_duration_days: number | null
|
||||
tariff: TariffInfo | null
|
||||
created_by: number | null
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
deep_link: string | null
|
||||
id: number;
|
||||
name: string;
|
||||
start_parameter: string;
|
||||
bonus_type: CampaignBonusType;
|
||||
is_active: boolean;
|
||||
balance_bonus_kopeks: number;
|
||||
balance_bonus_rubles: number;
|
||||
subscription_duration_days: number | null;
|
||||
subscription_traffic_gb: number | null;
|
||||
subscription_device_limit: number | null;
|
||||
subscription_squads: string[];
|
||||
tariff_id: number | null;
|
||||
tariff_duration_days: number | null;
|
||||
tariff: TariffInfo | null;
|
||||
created_by: number | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
deep_link: string | null;
|
||||
}
|
||||
|
||||
export interface CampaignCreateRequest {
|
||||
name: string
|
||||
start_parameter: string
|
||||
bonus_type: CampaignBonusType
|
||||
is_active?: boolean
|
||||
balance_bonus_kopeks?: number
|
||||
subscription_duration_days?: number
|
||||
subscription_traffic_gb?: number
|
||||
subscription_device_limit?: number
|
||||
subscription_squads?: string[]
|
||||
tariff_id?: number
|
||||
tariff_duration_days?: number
|
||||
name: string;
|
||||
start_parameter: string;
|
||||
bonus_type: CampaignBonusType;
|
||||
is_active?: boolean;
|
||||
balance_bonus_kopeks?: number;
|
||||
subscription_duration_days?: number;
|
||||
subscription_traffic_gb?: number;
|
||||
subscription_device_limit?: number;
|
||||
subscription_squads?: string[];
|
||||
tariff_id?: number;
|
||||
tariff_duration_days?: number;
|
||||
}
|
||||
|
||||
export interface CampaignUpdateRequest {
|
||||
name?: string
|
||||
start_parameter?: string
|
||||
bonus_type?: CampaignBonusType
|
||||
is_active?: boolean
|
||||
balance_bonus_kopeks?: number
|
||||
subscription_duration_days?: number
|
||||
subscription_traffic_gb?: number
|
||||
subscription_device_limit?: number
|
||||
subscription_squads?: string[]
|
||||
tariff_id?: number
|
||||
tariff_duration_days?: number
|
||||
name?: string;
|
||||
start_parameter?: string;
|
||||
bonus_type?: CampaignBonusType;
|
||||
is_active?: boolean;
|
||||
balance_bonus_kopeks?: number;
|
||||
subscription_duration_days?: number;
|
||||
subscription_traffic_gb?: number;
|
||||
subscription_device_limit?: number;
|
||||
subscription_squads?: string[];
|
||||
tariff_id?: number;
|
||||
tariff_duration_days?: number;
|
||||
}
|
||||
|
||||
export interface CampaignToggleResponse {
|
||||
id: number
|
||||
is_active: boolean
|
||||
message: string
|
||||
id: number;
|
||||
is_active: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CampaignStatistics {
|
||||
id: number
|
||||
name: string
|
||||
start_parameter: string
|
||||
bonus_type: CampaignBonusType
|
||||
is_active: boolean
|
||||
registrations: number
|
||||
balance_issued_kopeks: number
|
||||
balance_issued_rubles: number
|
||||
subscription_issued: number
|
||||
last_registration: string | null
|
||||
total_revenue_kopeks: number
|
||||
total_revenue_rubles: number
|
||||
avg_revenue_per_user_kopeks: number
|
||||
avg_revenue_per_user_rubles: number
|
||||
avg_first_payment_kopeks: number
|
||||
avg_first_payment_rubles: number
|
||||
trial_users_count: number
|
||||
active_trials_count: number
|
||||
conversion_count: number
|
||||
paid_users_count: number
|
||||
conversion_rate: number
|
||||
trial_conversion_rate: number
|
||||
deep_link: string | null
|
||||
id: number;
|
||||
name: string;
|
||||
start_parameter: string;
|
||||
bonus_type: CampaignBonusType;
|
||||
is_active: boolean;
|
||||
registrations: number;
|
||||
balance_issued_kopeks: number;
|
||||
balance_issued_rubles: number;
|
||||
subscription_issued: number;
|
||||
last_registration: string | null;
|
||||
total_revenue_kopeks: number;
|
||||
total_revenue_rubles: number;
|
||||
avg_revenue_per_user_kopeks: number;
|
||||
avg_revenue_per_user_rubles: number;
|
||||
avg_first_payment_kopeks: number;
|
||||
avg_first_payment_rubles: number;
|
||||
trial_users_count: number;
|
||||
active_trials_count: number;
|
||||
conversion_count: number;
|
||||
paid_users_count: number;
|
||||
conversion_rate: number;
|
||||
trial_conversion_rate: number;
|
||||
deep_link: string | null;
|
||||
}
|
||||
|
||||
export interface CampaignRegistrationItem {
|
||||
id: number
|
||||
user_id: number
|
||||
telegram_id: number
|
||||
username: string | null
|
||||
first_name: string | null
|
||||
bonus_type: string
|
||||
balance_bonus_kopeks: number
|
||||
subscription_duration_days: number | null
|
||||
tariff_id: number | null
|
||||
tariff_duration_days: number | null
|
||||
created_at: string
|
||||
user_balance_kopeks: number
|
||||
has_subscription: boolean
|
||||
has_paid: boolean
|
||||
id: number;
|
||||
user_id: number;
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
bonus_type: string;
|
||||
balance_bonus_kopeks: number;
|
||||
subscription_duration_days: number | null;
|
||||
tariff_id: number | null;
|
||||
tariff_duration_days: number | null;
|
||||
created_at: string;
|
||||
user_balance_kopeks: number;
|
||||
has_subscription: boolean;
|
||||
has_paid: boolean;
|
||||
}
|
||||
|
||||
export interface CampaignRegistrationsResponse {
|
||||
registrations: CampaignRegistrationItem[]
|
||||
total: number
|
||||
page: number
|
||||
per_page: number
|
||||
registrations: CampaignRegistrationItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
}
|
||||
|
||||
export interface CampaignsOverview {
|
||||
total: number
|
||||
active: number
|
||||
inactive: number
|
||||
total_registrations: number
|
||||
total_balance_issued_kopeks: number
|
||||
total_balance_issued_rubles: number
|
||||
total_subscription_issued: number
|
||||
total_tariff_issued: number
|
||||
total: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
total_registrations: number;
|
||||
total_balance_issued_kopeks: number;
|
||||
total_balance_issued_rubles: number;
|
||||
total_subscription_issued: number;
|
||||
total_tariff_issued: number;
|
||||
}
|
||||
|
||||
export interface ServerSquadInfo {
|
||||
id: number
|
||||
squad_uuid: string
|
||||
display_name: string
|
||||
country_code: string | null
|
||||
id: number;
|
||||
squad_uuid: string;
|
||||
display_name: string;
|
||||
country_code: string | null;
|
||||
}
|
||||
|
||||
export interface TariffListItem {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
traffic_limit_gb: number
|
||||
device_limit: number
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
is_active: boolean;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
}
|
||||
|
||||
export const campaignsApi = {
|
||||
// Get campaigns overview
|
||||
getOverview: async (): Promise<CampaignsOverview> => {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/overview')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/overview');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all campaigns
|
||||
getCampaigns: async (includeInactive = true, offset = 0, limit = 50): Promise<CampaignListResponse> => {
|
||||
getCampaigns: async (
|
||||
includeInactive = true,
|
||||
offset = 0,
|
||||
limit = 50,
|
||||
): Promise<CampaignListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns', {
|
||||
params: { include_inactive: includeInactive, offset, limit }
|
||||
})
|
||||
return response.data
|
||||
params: { include_inactive: includeInactive, offset, limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single campaign
|
||||
getCampaign: async (campaignId: number): Promise<CampaignDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get campaign statistics
|
||||
getCampaignStats: async (campaignId: number): Promise<CampaignStatistics> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/stats`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get campaign registrations
|
||||
getCampaignRegistrations: async (campaignId: number, page = 1, perPage = 50): Promise<CampaignRegistrationsResponse> => {
|
||||
getCampaignRegistrations: async (
|
||||
campaignId: number,
|
||||
page = 1,
|
||||
perPage = 50,
|
||||
): Promise<CampaignRegistrationsResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/registrations`, {
|
||||
params: { page, per_page: perPage }
|
||||
})
|
||||
return response.data
|
||||
params: { page, per_page: perPage },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create campaign
|
||||
createCampaign: async (data: CampaignCreateRequest): Promise<CampaignDetail> => {
|
||||
const response = await apiClient.post('/cabinet/admin/campaigns', data)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/campaigns', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update campaign
|
||||
updateCampaign: async (campaignId: number, data: CampaignUpdateRequest): Promise<CampaignDetail> => {
|
||||
const response = await apiClient.put(`/cabinet/admin/campaigns/${campaignId}`, data)
|
||||
return response.data
|
||||
updateCampaign: async (
|
||||
campaignId: number,
|
||||
data: CampaignUpdateRequest,
|
||||
): Promise<CampaignDetail> => {
|
||||
const response = await apiClient.put(`/cabinet/admin/campaigns/${campaignId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete campaign
|
||||
deleteCampaign: async (campaignId: number): Promise<{ message: string }> => {
|
||||
const response = await apiClient.delete(`/cabinet/admin/campaigns/${campaignId}`)
|
||||
return response.data
|
||||
const response = await apiClient.delete(`/cabinet/admin/campaigns/${campaignId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle campaign active status
|
||||
toggleCampaign: async (campaignId: number): Promise<CampaignToggleResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/campaigns/${campaignId}/toggle`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/campaigns/${campaignId}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available servers for subscription bonus
|
||||
getAvailableServers: async (): Promise<ServerSquadInfo[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-servers')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-servers');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available tariffs for tariff bonus
|
||||
getAvailableTariffs: async (): Promise<TariffListItem[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,163 +1,175 @@
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import { tokenStorage, isTokenExpired, tokenRefreshManager, safeRedirectToLogin } from '../utils/token'
|
||||
import { useBlockingStore } from '../store/blocking'
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import {
|
||||
tokenStorage,
|
||||
isTokenExpired,
|
||||
tokenRefreshManager,
|
||||
safeRedirectToLogin,
|
||||
} from '../utils/token';
|
||||
import { useBlockingStore } from '../store/blocking';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
// Настраиваем endpoint для refresh
|
||||
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/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'
|
||||
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
|
||||
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('')
|
||||
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()
|
||||
let token = getCsrfToken();
|
||||
if (!token) {
|
||||
token = generateCsrfToken()
|
||||
token = generateCsrfToken();
|
||||
// Set cookie with SameSite=Strict for CSRF protection
|
||||
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`
|
||||
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`;
|
||||
}
|
||||
return token
|
||||
return token;
|
||||
}
|
||||
|
||||
const getTelegramInitData = (): string | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const initData = window.Telegram?.WebApp?.initData
|
||||
const initData = window.Telegram?.WebApp?.initData;
|
||||
if (initData) {
|
||||
tokenStorage.setTelegramInitData(initData)
|
||||
return initData
|
||||
tokenStorage.setTelegramInitData(initData);
|
||||
return initData;
|
||||
}
|
||||
|
||||
return tokenStorage.getTelegramInitData()
|
||||
}
|
||||
return tokenStorage.getTelegramInitData();
|
||||
};
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Request interceptor - add auth token with expiration check
|
||||
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||||
let token = tokenStorage.getAccessToken()
|
||||
let token = tokenStorage.getAccessToken();
|
||||
|
||||
// Проверяем срок действия токена перед запросом
|
||||
if (token && isTokenExpired(token)) {
|
||||
// Используем централизованный менеджер для refresh
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken()
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken
|
||||
token = newToken;
|
||||
} else {
|
||||
// Refresh не удался - редирект на логин
|
||||
tokenStorage.clearTokens()
|
||||
safeRedirectToLogin()
|
||||
return config
|
||||
tokenStorage.clearTokens();
|
||||
safeRedirectToLogin();
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const telegramInitData = getTelegramInitData()
|
||||
const telegramInitData = getTelegramInitData();
|
||||
if (telegramInitData && config.headers) {
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData;
|
||||
}
|
||||
|
||||
// Add CSRF token for state-changing methods
|
||||
const method = config.method?.toUpperCase()
|
||||
const method = config.method?.toUpperCase();
|
||||
if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) {
|
||||
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken()
|
||||
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken();
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
return config;
|
||||
});
|
||||
|
||||
// Custom error types for special handling
|
||||
export interface MaintenanceError {
|
||||
code: 'maintenance'
|
||||
message: string
|
||||
reason?: string
|
||||
code: 'maintenance';
|
||||
message: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ChannelSubscriptionError {
|
||||
code: 'channel_subscription_required'
|
||||
message: string
|
||||
channel_link?: string
|
||||
code: 'channel_subscription_required';
|
||||
message: string;
|
||||
channel_link?: string;
|
||||
}
|
||||
|
||||
export function isMaintenanceError(error: unknown): error is { response: { status: 503, data: { detail: MaintenanceError } } } {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
const err = error as AxiosError<{ detail: MaintenanceError }>
|
||||
return err.response?.status === 503 && err.response?.data?.detail?.code === 'maintenance'
|
||||
export function isMaintenanceError(
|
||||
error: unknown,
|
||||
): error is { response: { status: 503; data: { detail: MaintenanceError } } } {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
const err = error as AxiosError<{ detail: MaintenanceError }>;
|
||||
return err.response?.status === 503 && err.response?.data?.detail?.code === 'maintenance';
|
||||
}
|
||||
|
||||
export function isChannelSubscriptionError(error: unknown): error is { response: { status: 403, data: { detail: ChannelSubscriptionError } } } {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
const err = error as AxiosError<{ detail: ChannelSubscriptionError }>
|
||||
return err.response?.status === 403 && err.response?.data?.detail?.code === 'channel_subscription_required'
|
||||
export function isChannelSubscriptionError(
|
||||
error: unknown,
|
||||
): error is { response: { status: 403; data: { detail: ChannelSubscriptionError } } } {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
const err = error as AxiosError<{ detail: ChannelSubscriptionError }>;
|
||||
return (
|
||||
err.response?.status === 403 &&
|
||||
err.response?.data?.detail?.code === 'channel_subscription_required'
|
||||
);
|
||||
}
|
||||
|
||||
// Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription)
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Handle maintenance mode (503)
|
||||
if (isMaintenanceError(error)) {
|
||||
const detail = (error.response?.data as { detail: MaintenanceError }).detail
|
||||
const detail = (error.response?.data as { detail: MaintenanceError }).detail;
|
||||
useBlockingStore.getState().setMaintenance({
|
||||
message: detail.message,
|
||||
reason: detail.reason,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle channel subscription required (403)
|
||||
if (isChannelSubscriptionError(error)) {
|
||||
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail
|
||||
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail;
|
||||
useBlockingStore.getState().setChannelSubscription({
|
||||
message: detail.message,
|
||||
channel_link: detail.channel_link,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала)
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
originalRequest._retry = true;
|
||||
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken()
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
}
|
||||
return apiClient(originalRequest)
|
||||
return apiClient(originalRequest);
|
||||
} else {
|
||||
// Refresh не удался
|
||||
tokenStorage.clearTokens()
|
||||
safeRedirectToLogin()
|
||||
tokenStorage.clearTokens();
|
||||
safeRedirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default apiClient
|
||||
export default apiClient;
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface ContestInfo {
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
description: string | null
|
||||
prize_days: number
|
||||
is_available: boolean
|
||||
already_played: boolean
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
prize_days: number;
|
||||
is_available: boolean;
|
||||
already_played: boolean;
|
||||
}
|
||||
|
||||
export interface ContestGameData {
|
||||
round_id: number
|
||||
game_type: string
|
||||
game_data: Record<string, any>
|
||||
instructions: string
|
||||
round_id: number;
|
||||
game_type: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
game_data: Record<string, any>;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
export interface ContestResult {
|
||||
is_winner: boolean
|
||||
message: string
|
||||
prize_days?: number
|
||||
is_winner: boolean;
|
||||
message: string;
|
||||
prize_days?: number;
|
||||
}
|
||||
|
||||
export interface ContestsCountResponse {
|
||||
count: number
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const contestsApi = {
|
||||
// Get count of available contests
|
||||
getCount: async (): Promise<ContestsCountResponse> => {
|
||||
const response = await apiClient.get<ContestsCountResponse>('/cabinet/contests/count')
|
||||
return response.data
|
||||
const response = await apiClient.get<ContestsCountResponse>('/cabinet/contests/count');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get list of available contests
|
||||
getContests: async (): Promise<ContestInfo[]> => {
|
||||
const response = await apiClient.get<ContestInfo[]>('/cabinet/contests')
|
||||
return response.data
|
||||
const response = await apiClient.get<ContestInfo[]>('/cabinet/contests');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get game data for a specific contest
|
||||
getContestGame: async (roundId: number): Promise<ContestGameData> => {
|
||||
const response = await apiClient.get<ContestGameData>(`/cabinet/contests/${roundId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<ContestGameData>(`/cabinet/contests/${roundId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Submit answer for a contest
|
||||
@@ -51,7 +52,7 @@ export const contestsApi = {
|
||||
const response = await apiClient.post<ContestResult>(`/cabinet/contests/${roundId}/answer`, {
|
||||
round_id: roundId,
|
||||
answer,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
// Currency exchange rate API
|
||||
// Uses free exchangerate.host API
|
||||
|
||||
import axios from 'axios'
|
||||
import axios from 'axios';
|
||||
|
||||
interface ExchangeRates {
|
||||
USD: number
|
||||
CNY: number
|
||||
IRR: number
|
||||
USD: number;
|
||||
CNY: number;
|
||||
IRR: number;
|
||||
}
|
||||
|
||||
interface CachedRates {
|
||||
rates: ExchangeRates
|
||||
timestamp: number
|
||||
rates: ExchangeRates;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const CACHE_DURATION = 60 * 60 * 1000 // 1 hour in milliseconds
|
||||
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
|
||||
// Fallback rates if API fails (approximate)
|
||||
const FALLBACK_RATES: ExchangeRates = {
|
||||
USD: 100, // 1 USD = 100 RUB
|
||||
CNY: 14, // 1 CNY = 14 RUB
|
||||
IRR: 0.0024, // 1 IRR = 0.0024 RUB (or 1 RUB = ~420 IRR)
|
||||
}
|
||||
USD: 100, // 1 USD = 100 RUB
|
||||
CNY: 14, // 1 CNY = 14 RUB
|
||||
IRR: 0.0024, // 1 IRR = 0.0024 RUB (or 1 RUB = ~420 IRR)
|
||||
};
|
||||
|
||||
let cachedRates: CachedRates | null = null
|
||||
let cachedRates: CachedRates | null = null;
|
||||
|
||||
export const currencyApi = {
|
||||
// Get all exchange rates (RUB to other currencies)
|
||||
getExchangeRates: async (): Promise<ExchangeRates> => {
|
||||
// Check cache first
|
||||
if (cachedRates && Date.now() - cachedRates.timestamp < CACHE_DURATION) {
|
||||
return cachedRates.rates
|
||||
return cachedRates.rates;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try primary API (exchangerate.host) - get RUB based rates
|
||||
const response = await axios.get<{
|
||||
success?: boolean
|
||||
rates?: { USD?: number; CNY?: number; IRR?: number }
|
||||
success?: boolean;
|
||||
rates?: { USD?: number; CNY?: number; IRR?: number };
|
||||
}>('https://api.exchangerate.host/latest', {
|
||||
params: { base: 'RUB', symbols: 'USD,CNY,IRR' },
|
||||
})
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.rates) {
|
||||
// API returns how much of each currency equals 1 RUB
|
||||
@@ -49,51 +49,65 @@ export const currencyApi = {
|
||||
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
|
||||
};
|
||||
cachedRates = { rates, timestamp: Date.now() };
|
||||
return rates;
|
||||
}
|
||||
|
||||
// Try backup API (open.er-api.com)
|
||||
const backupResponse = await axios.get<{
|
||||
rates?: { USD?: number; CNY?: number; IRR?: number }
|
||||
}>('https://open.er-api.com/v6/latest/RUB')
|
||||
rates?: { USD?: number; CNY?: number; IRR?: number };
|
||||
}>('https://open.er-api.com/v6/latest/RUB');
|
||||
|
||||
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
|
||||
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
|
||||
return FALLBACK_RATES
|
||||
return FALLBACK_RATES;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch exchange rates, using fallback:', error)
|
||||
return FALLBACK_RATES
|
||||
console.warn('Failed to fetch exchange rates, using fallback:', error);
|
||||
return FALLBACK_RATES;
|
||||
}
|
||||
},
|
||||
|
||||
// Convert RUB to target currency
|
||||
convertFromRub: (rubAmount: number, targetCurrency: keyof ExchangeRates, rates: ExchangeRates): number => {
|
||||
const rate = rates[targetCurrency]
|
||||
convertFromRub: (
|
||||
rubAmount: number,
|
||||
targetCurrency: keyof ExchangeRates,
|
||||
rates: ExchangeRates,
|
||||
): number => {
|
||||
const rate = rates[targetCurrency];
|
||||
if (!rate || rate <= 0) {
|
||||
return rubAmount / FALLBACK_RATES[targetCurrency]
|
||||
return rubAmount / FALLBACK_RATES[targetCurrency];
|
||||
}
|
||||
return rubAmount / rate
|
||||
return rubAmount / rate;
|
||||
},
|
||||
|
||||
// Convert from target currency to RUB
|
||||
convertToRub: (amount: number, sourceCurrency: keyof ExchangeRates, rates: ExchangeRates): number => {
|
||||
const rate = rates[sourceCurrency]
|
||||
convertToRub: (
|
||||
amount: number,
|
||||
sourceCurrency: keyof ExchangeRates,
|
||||
rates: ExchangeRates,
|
||||
): number => {
|
||||
const rate = rates[sourceCurrency];
|
||||
if (!rate || rate <= 0) {
|
||||
return amount * FALLBACK_RATES[sourceCurrency]
|
||||
return amount * FALLBACK_RATES[sourceCurrency];
|
||||
}
|
||||
return amount * rate
|
||||
return amount * rate;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export type { ExchangeRates }
|
||||
export type { ExchangeRates };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export { apiClient } from './client'
|
||||
export { authApi } from './auth'
|
||||
export { subscriptionApi } from './subscription'
|
||||
export { balanceApi } from './balance'
|
||||
export { referralApi } from './referral'
|
||||
export { ticketsApi } from './tickets'
|
||||
export { contestsApi } from './contests'
|
||||
export { pollsApi } from './polls'
|
||||
export { promoApi } from './promo'
|
||||
export { notificationsApi } from './notifications'
|
||||
export { infoApi } from './info'
|
||||
export { adminSettingsApi } from './adminSettings'
|
||||
export { apiClient } from './client';
|
||||
export { authApi } from './auth';
|
||||
export { subscriptionApi } from './subscription';
|
||||
export { balanceApi } from './balance';
|
||||
export { referralApi } from './referral';
|
||||
export { ticketsApi } from './tickets';
|
||||
export { contestsApi } from './contests';
|
||||
export { pollsApi } from './polls';
|
||||
export { promoApi } from './promo';
|
||||
export { notificationsApi } from './notifications';
|
||||
export { infoApi } from './info';
|
||||
export { adminSettingsApi } from './adminSettings';
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import apiClient from './client'
|
||||
import type { SupportConfig } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { SupportConfig } from '../types';
|
||||
|
||||
export interface FaqPage {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
order: number
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface RulesResponse {
|
||||
content: string
|
||||
updated_at: string | null
|
||||
content: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface PrivacyPolicyResponse {
|
||||
content: string
|
||||
updated_at: string | null
|
||||
content: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface PublicOfferResponse {
|
||||
content: string
|
||||
updated_at: string | null
|
||||
content: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceInfo {
|
||||
name: string
|
||||
description: string | null
|
||||
support_email: string | null
|
||||
support_telegram: string | null
|
||||
website: string | null
|
||||
name: string;
|
||||
description: string | null;
|
||||
support_email: string | null;
|
||||
support_telegram: string | null;
|
||||
website: string | null;
|
||||
}
|
||||
|
||||
export interface LanguageInfo {
|
||||
code: string
|
||||
name: string
|
||||
flag: string
|
||||
code: string;
|
||||
name: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
export const infoApi = {
|
||||
// Get FAQ pages list
|
||||
getFaqPages: async (): Promise<FaqPage[]> => {
|
||||
const response = await apiClient.get<FaqPage[]>('/cabinet/info/faq')
|
||||
return response.data
|
||||
const response = await apiClient.get<FaqPage[]>('/cabinet/info/faq');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get specific FAQ page
|
||||
getFaqPage: async (pageId: number): Promise<FaqPage> => {
|
||||
const response = await apiClient.get<FaqPage>(`/cabinet/info/faq/${pageId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<FaqPage>(`/cabinet/info/faq/${pageId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get service rules
|
||||
getRules: async (): Promise<RulesResponse> => {
|
||||
const response = await apiClient.get<RulesResponse>('/cabinet/info/rules')
|
||||
return response.data
|
||||
const response = await apiClient.get<RulesResponse>('/cabinet/info/rules');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get privacy policy
|
||||
getPrivacyPolicy: async (): Promise<PrivacyPolicyResponse> => {
|
||||
const response = await apiClient.get<PrivacyPolicyResponse>('/cabinet/info/privacy-policy')
|
||||
return response.data
|
||||
const response = await apiClient.get<PrivacyPolicyResponse>('/cabinet/info/privacy-policy');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get public offer
|
||||
getPublicOffer: async (): Promise<PublicOfferResponse> => {
|
||||
const response = await apiClient.get<PublicOfferResponse>('/cabinet/info/public-offer')
|
||||
return response.data
|
||||
const response = await apiClient.get<PublicOfferResponse>('/cabinet/info/public-offer');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get service info
|
||||
getServiceInfo: async (): Promise<ServiceInfo> => {
|
||||
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service')
|
||||
return response.data
|
||||
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available languages
|
||||
getLanguages: async (): Promise<{ languages: LanguageInfo[]; default: string }> => {
|
||||
const response = await apiClient.get('/cabinet/info/languages')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/info/languages');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get user language
|
||||
getUserLanguage: async (): Promise<{ language: string }> => {
|
||||
const response = await apiClient.get<{ language: string }>('/cabinet/info/user/language')
|
||||
return response.data
|
||||
const response = await apiClient.get<{ language: string }>('/cabinet/info/user/language');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update user language
|
||||
updateUserLanguage: async (language: string): Promise<{ language: string }> => {
|
||||
const response = await apiClient.patch<{ language: string }>('/cabinet/info/user/language', {
|
||||
language,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get support configuration
|
||||
getSupportConfig: async (): Promise<SupportConfig> => {
|
||||
const response = await apiClient.get<SupportConfig>('/cabinet/info/support-config')
|
||||
return response.data
|
||||
const response = await apiClient.get<SupportConfig>('/cabinet/info/support-config');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
export interface MiniappCreatePaymentPayload {
|
||||
initData: string
|
||||
method: string
|
||||
amountKopeks?: number | null
|
||||
option?: string | null
|
||||
initData: string;
|
||||
method: string;
|
||||
amountKopeks?: number | null;
|
||||
option?: string | null;
|
||||
}
|
||||
|
||||
export interface MiniappCreatePaymentResponse {
|
||||
payment_url: string
|
||||
amount_kopeks?: number
|
||||
extra?: Record<string, unknown>
|
||||
payment_url: string;
|
||||
amount_kopeks?: number;
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const miniappApi = {
|
||||
// Create payment inside Telegram Mini App (same flow as miniapp/index.html)
|
||||
createPayment: async (
|
||||
payload: MiniappCreatePaymentPayload
|
||||
payload: MiniappCreatePaymentPayload,
|
||||
): Promise<MiniappCreatePaymentResponse> => {
|
||||
try {
|
||||
const response = await axios.post<MiniappCreatePaymentResponse>(
|
||||
@@ -29,15 +29,16 @@ export const miniappApi = {
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
);
|
||||
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)
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,58 +1,64 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface NotificationSettings {
|
||||
subscription_expiry_enabled: boolean
|
||||
subscription_expiry_days: number
|
||||
traffic_warning_enabled: boolean
|
||||
traffic_warning_percent: number
|
||||
balance_low_enabled: boolean
|
||||
balance_low_threshold: number
|
||||
news_enabled: boolean
|
||||
promo_offers_enabled: boolean
|
||||
subscription_expiry_enabled: boolean;
|
||||
subscription_expiry_days: number;
|
||||
traffic_warning_enabled: boolean;
|
||||
traffic_warning_percent: number;
|
||||
balance_low_enabled: boolean;
|
||||
balance_low_threshold: number;
|
||||
news_enabled: boolean;
|
||||
promo_offers_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationSettingsUpdate {
|
||||
subscription_expiry_enabled?: boolean
|
||||
subscription_expiry_days?: number
|
||||
traffic_warning_enabled?: boolean
|
||||
traffic_warning_percent?: number
|
||||
balance_low_enabled?: boolean
|
||||
balance_low_threshold?: number
|
||||
news_enabled?: boolean
|
||||
promo_offers_enabled?: boolean
|
||||
subscription_expiry_enabled?: boolean;
|
||||
subscription_expiry_days?: number;
|
||||
traffic_warning_enabled?: boolean;
|
||||
traffic_warning_percent?: number;
|
||||
balance_low_enabled?: boolean;
|
||||
balance_low_threshold?: number;
|
||||
news_enabled?: boolean;
|
||||
promo_offers_enabled?: boolean;
|
||||
}
|
||||
|
||||
export const notificationsApi = {
|
||||
// Get notification settings
|
||||
getSettings: async (): Promise<NotificationSettings> => {
|
||||
const response = await apiClient.get<NotificationSettings>('/cabinet/notifications')
|
||||
return response.data
|
||||
const response = await apiClient.get<NotificationSettings>('/cabinet/notifications');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update notification settings
|
||||
updateSettings: async (settings: NotificationSettingsUpdate): Promise<NotificationSettings> => {
|
||||
const response = await apiClient.patch<NotificationSettings>('/cabinet/notifications', settings)
|
||||
return response.data
|
||||
const response = await apiClient.patch<NotificationSettings>(
|
||||
'/cabinet/notifications',
|
||||
settings,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Send test notification
|
||||
sendTestNotification: async (): Promise<{ success: boolean; message: string }> => {
|
||||
const response = await apiClient.post<{ success: boolean; message: string }>(
|
||||
'/cabinet/notifications/test'
|
||||
)
|
||||
return response.data
|
||||
'/cabinet/notifications/test',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get notification history
|
||||
getHistory: async (limit = 20, offset = 0): Promise<{
|
||||
notifications: any[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
getHistory: async (
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
): Promise<{
|
||||
notifications: unknown[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/notifications/history', {
|
||||
params: { limit, offset },
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface PollOption {
|
||||
id: number
|
||||
text: string
|
||||
order: number
|
||||
id: number;
|
||||
text: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface PollQuestion {
|
||||
id: number
|
||||
text: string
|
||||
order: number
|
||||
options: PollOption[]
|
||||
id: number;
|
||||
text: string;
|
||||
order: number;
|
||||
options: PollOption[];
|
||||
}
|
||||
|
||||
export interface PollInfo {
|
||||
id: number
|
||||
response_id: number
|
||||
title: string
|
||||
description: string | null
|
||||
total_questions: number
|
||||
answered_questions: number
|
||||
is_completed: boolean
|
||||
reward_amount: number | null
|
||||
id: number;
|
||||
response_id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
total_questions: number;
|
||||
answered_questions: number;
|
||||
is_completed: boolean;
|
||||
reward_amount: number | null;
|
||||
}
|
||||
|
||||
export interface PollStartResponse {
|
||||
response_id: number
|
||||
current_question_index: number
|
||||
total_questions: number
|
||||
question: PollQuestion
|
||||
response_id: number;
|
||||
current_question_index: number;
|
||||
total_questions: number;
|
||||
question: PollQuestion;
|
||||
}
|
||||
|
||||
export interface PollAnswerResponse {
|
||||
success: boolean
|
||||
is_completed: boolean
|
||||
next_question: PollQuestion | null
|
||||
current_question_index: number | null
|
||||
total_questions: number
|
||||
reward_granted: number | null
|
||||
message: string | null
|
||||
success: boolean;
|
||||
is_completed: boolean;
|
||||
next_question: PollQuestion | null;
|
||||
current_question_index: number | null;
|
||||
total_questions: number;
|
||||
reward_granted: number | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface PollsCountResponse {
|
||||
count: number
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const pollsApi = {
|
||||
// Get count of available polls
|
||||
getCount: async (): Promise<PollsCountResponse> => {
|
||||
const response = await apiClient.get<PollsCountResponse>('/cabinet/polls/count')
|
||||
return response.data
|
||||
const response = await apiClient.get<PollsCountResponse>('/cabinet/polls/count');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available polls
|
||||
getPolls: async (): Promise<PollInfo[]> => {
|
||||
const response = await apiClient.get<PollInfo[]>('/cabinet/polls')
|
||||
return response.data
|
||||
const response = await apiClient.get<PollInfo[]>('/cabinet/polls');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get poll details
|
||||
getPollDetails: async (responseId: number): Promise<PollInfo> => {
|
||||
const response = await apiClient.get<PollInfo>(`/cabinet/polls/${responseId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<PollInfo>(`/cabinet/polls/${responseId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Start or continue poll
|
||||
startPoll: async (responseId: number): Promise<PollStartResponse> => {
|
||||
const response = await apiClient.post<PollStartResponse>(`/cabinet/polls/${responseId}/start`)
|
||||
return response.data
|
||||
const response = await apiClient.post<PollStartResponse>(`/cabinet/polls/${responseId}/start`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Answer a question
|
||||
answerQuestion: async (
|
||||
responseId: number,
|
||||
questionId: number,
|
||||
optionId: number
|
||||
optionId: number,
|
||||
): Promise<PollAnswerResponse> => {
|
||||
const response = await apiClient.post<PollAnswerResponse>(
|
||||
`/cabinet/polls/${responseId}/questions/${questionId}/answer`,
|
||||
{ option_id: optionId }
|
||||
)
|
||||
return response.data
|
||||
{ option_id: optionId },
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
103
src/api/promo.ts
103
src/api/promo.ts
@@ -1,96 +1,97 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
export interface PromoOffer {
|
||||
id: number
|
||||
notification_type: string
|
||||
discount_percent: number | null
|
||||
effect_type: string
|
||||
expires_at: string
|
||||
is_active: boolean
|
||||
is_claimed: boolean
|
||||
claimed_at: string | null
|
||||
extra_data: Record<string, any> | null
|
||||
id: number;
|
||||
notification_type: string;
|
||||
discount_percent: number | null;
|
||||
effect_type: string;
|
||||
expires_at: string;
|
||||
is_active: boolean;
|
||||
is_claimed: boolean;
|
||||
claimed_at: string | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
extra_data: Record<string, any> | null;
|
||||
}
|
||||
|
||||
export interface ActiveDiscount {
|
||||
discount_percent: number
|
||||
source: string | null
|
||||
expires_at: string | null
|
||||
is_active: boolean
|
||||
discount_percent: number;
|
||||
source: string | null;
|
||||
expires_at: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface PromoGroupDiscounts {
|
||||
group_name: string | null
|
||||
server_discount_percent: number
|
||||
traffic_discount_percent: number
|
||||
device_discount_percent: number
|
||||
period_discounts: Record<string, number>
|
||||
group_name: string | null;
|
||||
server_discount_percent: number;
|
||||
traffic_discount_percent: number;
|
||||
device_discount_percent: number;
|
||||
period_discounts: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ClaimOfferResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
discount_percent: number | null
|
||||
expires_at: string | null
|
||||
success: boolean;
|
||||
message: string;
|
||||
discount_percent: number | null;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export interface LoyaltyTierInfo {
|
||||
id: number
|
||||
name: string
|
||||
threshold_rubles: number
|
||||
server_discount_percent: number
|
||||
traffic_discount_percent: number
|
||||
device_discount_percent: number
|
||||
period_discounts: Record<string, number>
|
||||
is_current: boolean
|
||||
is_achieved: boolean
|
||||
id: number;
|
||||
name: string;
|
||||
threshold_rubles: number;
|
||||
server_discount_percent: number;
|
||||
traffic_discount_percent: number;
|
||||
device_discount_percent: number;
|
||||
period_discounts: Record<string, number>;
|
||||
is_current: boolean;
|
||||
is_achieved: boolean;
|
||||
}
|
||||
|
||||
export interface LoyaltyTiersResponse {
|
||||
tiers: LoyaltyTierInfo[]
|
||||
current_spent_rubles: number
|
||||
current_tier_name: string | null
|
||||
next_tier_name: string | null
|
||||
next_tier_threshold_rubles: number | null
|
||||
progress_percent: number
|
||||
tiers: LoyaltyTierInfo[];
|
||||
current_spent_rubles: number;
|
||||
current_tier_name: string | null;
|
||||
next_tier_name: string | null;
|
||||
next_tier_threshold_rubles: number | null;
|
||||
progress_percent: number;
|
||||
}
|
||||
|
||||
export const promoApi = {
|
||||
// Get available promo offers
|
||||
getOffers: async (): Promise<PromoOffer[]> => {
|
||||
const response = await apiClient.get<PromoOffer[]>('/cabinet/promo/offers')
|
||||
return response.data
|
||||
const response = await apiClient.get<PromoOffer[]>('/cabinet/promo/offers');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get active discount
|
||||
getActiveDiscount: async (): Promise<ActiveDiscount> => {
|
||||
const response = await apiClient.get<ActiveDiscount>('/cabinet/promo/active-discount')
|
||||
return response.data
|
||||
const response = await apiClient.get<ActiveDiscount>('/cabinet/promo/active-discount');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get promo group discounts
|
||||
getGroupDiscounts: async (): Promise<PromoGroupDiscounts> => {
|
||||
const response = await apiClient.get<PromoGroupDiscounts>('/cabinet/promo/group-discounts')
|
||||
return response.data
|
||||
const response = await apiClient.get<PromoGroupDiscounts>('/cabinet/promo/group-discounts');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get loyalty tiers (promo groups with spending thresholds)
|
||||
getLoyaltyTiers: async (): Promise<LoyaltyTiersResponse> => {
|
||||
const response = await apiClient.get<LoyaltyTiersResponse>('/cabinet/promo/loyalty-tiers')
|
||||
return response.data
|
||||
const response = await apiClient.get<LoyaltyTiersResponse>('/cabinet/promo/loyalty-tiers');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Claim a promo offer
|
||||
claimOffer: async (offerId: number): Promise<ClaimOfferResponse> => {
|
||||
const response = await apiClient.post<ClaimOfferResponse>('/cabinet/promo/claim', {
|
||||
offer_id: offerId,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Clear active discount
|
||||
clearActiveDiscount: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.delete<{ message: string }>('/cabinet/promo/active-discount')
|
||||
return response.data
|
||||
const response = await apiClient.delete<{ message: string }>('/cabinet/promo/active-discount');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,139 +1,140 @@
|
||||
import apiClient from './client'
|
||||
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
|
||||
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
|
||||
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
|
||||
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;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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
|
||||
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
|
||||
notification_type: string;
|
||||
valid_hours: number;
|
||||
discount_percent?: number;
|
||||
bonus_amount_kopeks?: number;
|
||||
effect_type?: string;
|
||||
extra_data?: Record<string, unknown>;
|
||||
target?: string;
|
||||
user_id?: number;
|
||||
telegram_id?: number;
|
||||
// Telegram notification options
|
||||
send_notification?: boolean
|
||||
message_text?: string
|
||||
button_text?: string
|
||||
send_notification?: boolean;
|
||||
message_text?: string;
|
||||
button_text?: string;
|
||||
}
|
||||
|
||||
export interface PromoOfferBroadcastResponse {
|
||||
created_offers: number
|
||||
user_ids: number[]
|
||||
target: string | null
|
||||
notifications_sent: number
|
||||
notifications_failed: number
|
||||
created_offers: number;
|
||||
user_ids: number[];
|
||||
target: string | null;
|
||||
notifications_sent: number;
|
||||
notifications_failed: number;
|
||||
}
|
||||
|
||||
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
|
||||
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[]
|
||||
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
|
||||
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
|
||||
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
|
||||
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, unknown>;
|
||||
created_at: string;
|
||||
user: PromoOfferUserInfo | null;
|
||||
offer: PromoOfferLogOfferInfo | null;
|
||||
}
|
||||
|
||||
export interface PromoOfferLogListResponse {
|
||||
items: PromoOfferLog[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
items: PromoOfferLog[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
// Target segments for broadcast
|
||||
@@ -154,9 +155,9 @@ export const TARGET_SEGMENTS = {
|
||||
custom_week: 'Зарегистрированы за неделю',
|
||||
custom_month: 'Зарегистрированы за месяц',
|
||||
custom_active_today: 'Активны сегодня',
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export type TargetSegment = keyof typeof TARGET_SEGMENTS
|
||||
export type TargetSegment = keyof typeof TARGET_SEGMENTS;
|
||||
|
||||
// Offer type configurations
|
||||
export const OFFER_TYPE_CONFIG = {
|
||||
@@ -178,56 +179,61 @@ export const OFFER_TYPE_CONFIG = {
|
||||
effect: 'percent_discount',
|
||||
description: 'Скидка для новых пользователей',
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export type OfferType = keyof typeof OFFER_TYPE_CONFIG
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
updateTemplate: async (
|
||||
id: number,
|
||||
data: PromoOfferTemplateUpdateRequest,
|
||||
): Promise<PromoOfferTemplate> => {
|
||||
const response = await apiClient.patch(`/cabinet/admin/promo-offers/templates/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,122 +1,127 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// ============== Types ==============
|
||||
|
||||
export type PromoCodeType = 'balance' | 'subscription_days' | 'trial_subscription' | 'promo_group' | 'discount'
|
||||
export type PromoCodeType =
|
||||
| 'balance'
|
||||
| 'subscription_days'
|
||||
| 'trial_subscription'
|
||||
| 'promo_group'
|
||||
| 'discount';
|
||||
|
||||
export interface PromoCode {
|
||||
id: number
|
||||
code: string
|
||||
type: PromoCodeType
|
||||
balance_bonus_kopeks: number
|
||||
balance_bonus_rubles: number
|
||||
subscription_days: number
|
||||
max_uses: number
|
||||
current_uses: number
|
||||
uses_left: number
|
||||
is_active: boolean
|
||||
is_valid: boolean
|
||||
first_purchase_only: boolean
|
||||
valid_from: string
|
||||
valid_until: string | null
|
||||
promo_group_id: number | null
|
||||
created_by: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
id: number;
|
||||
code: string;
|
||||
type: PromoCodeType;
|
||||
balance_bonus_kopeks: number;
|
||||
balance_bonus_rubles: number;
|
||||
subscription_days: number;
|
||||
max_uses: number;
|
||||
current_uses: number;
|
||||
uses_left: number;
|
||||
is_active: boolean;
|
||||
is_valid: boolean;
|
||||
first_purchase_only: boolean;
|
||||
valid_from: string;
|
||||
valid_until: string | null;
|
||||
promo_group_id: number | null;
|
||||
created_by: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PromoCodeRecentUse {
|
||||
id: number
|
||||
user_id: number
|
||||
user_username: string | null
|
||||
user_full_name: string | null
|
||||
user_telegram_id: number | null
|
||||
used_at: string
|
||||
id: number;
|
||||
user_id: number;
|
||||
user_username: string | null;
|
||||
user_full_name: string | null;
|
||||
user_telegram_id: number | null;
|
||||
used_at: string;
|
||||
}
|
||||
|
||||
export interface PromoCodeDetail extends PromoCode {
|
||||
total_uses: number
|
||||
today_uses: number
|
||||
recent_uses: PromoCodeRecentUse[]
|
||||
total_uses: number;
|
||||
today_uses: number;
|
||||
recent_uses: PromoCodeRecentUse[];
|
||||
}
|
||||
|
||||
export interface PromoCodeListResponse {
|
||||
items: PromoCode[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
items: PromoCode[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface PromoCodeCreateRequest {
|
||||
code: string
|
||||
type: PromoCodeType
|
||||
balance_bonus_kopeks?: number
|
||||
subscription_days?: number
|
||||
max_uses?: number
|
||||
valid_from?: string
|
||||
valid_until?: string | null
|
||||
is_active?: boolean
|
||||
first_purchase_only?: boolean
|
||||
promo_group_id?: number | null
|
||||
code: string;
|
||||
type: PromoCodeType;
|
||||
balance_bonus_kopeks?: number;
|
||||
subscription_days?: number;
|
||||
max_uses?: number;
|
||||
valid_from?: string;
|
||||
valid_until?: string | null;
|
||||
is_active?: boolean;
|
||||
first_purchase_only?: boolean;
|
||||
promo_group_id?: number | null;
|
||||
}
|
||||
|
||||
export interface PromoCodeUpdateRequest {
|
||||
code?: string
|
||||
type?: PromoCodeType
|
||||
balance_bonus_kopeks?: number
|
||||
subscription_days?: number
|
||||
max_uses?: number
|
||||
valid_from?: string
|
||||
valid_until?: string | null
|
||||
is_active?: boolean
|
||||
first_purchase_only?: boolean
|
||||
promo_group_id?: number | null
|
||||
code?: string;
|
||||
type?: PromoCodeType;
|
||||
balance_bonus_kopeks?: number;
|
||||
subscription_days?: number;
|
||||
max_uses?: number;
|
||||
valid_from?: string;
|
||||
valid_until?: string | null;
|
||||
is_active?: boolean;
|
||||
first_purchase_only?: boolean;
|
||||
promo_group_id?: number | null;
|
||||
}
|
||||
|
||||
// ============== PromoGroup Types ==============
|
||||
|
||||
export interface PromoGroup {
|
||||
id: number
|
||||
name: string
|
||||
server_discount_percent: number
|
||||
traffic_discount_percent: number
|
||||
device_discount_percent: number
|
||||
period_discounts: Record<number, number>
|
||||
auto_assign_total_spent_kopeks: number | null
|
||||
apply_discounts_to_addons: boolean
|
||||
is_default: boolean
|
||||
members_count: number
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
id: number;
|
||||
name: string;
|
||||
server_discount_percent: number;
|
||||
traffic_discount_percent: number;
|
||||
device_discount_percent: number;
|
||||
period_discounts: Record<number, number>;
|
||||
auto_assign_total_spent_kopeks: number | null;
|
||||
apply_discounts_to_addons: boolean;
|
||||
is_default: boolean;
|
||||
members_count: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface PromoGroupListResponse {
|
||||
items: PromoGroup[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
items: PromoGroup[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface PromoGroupCreateRequest {
|
||||
name: string
|
||||
server_discount_percent?: number
|
||||
traffic_discount_percent?: number
|
||||
device_discount_percent?: number
|
||||
period_discounts?: Record<number, number>
|
||||
auto_assign_total_spent_kopeks?: number | null
|
||||
apply_discounts_to_addons?: boolean
|
||||
is_default?: boolean
|
||||
name: string;
|
||||
server_discount_percent?: number;
|
||||
traffic_discount_percent?: number;
|
||||
device_discount_percent?: number;
|
||||
period_discounts?: Record<number, number>;
|
||||
auto_assign_total_spent_kopeks?: number | null;
|
||||
apply_discounts_to_addons?: boolean;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
export interface PromoGroupUpdateRequest {
|
||||
name?: string
|
||||
server_discount_percent?: number
|
||||
traffic_discount_percent?: number
|
||||
device_discount_percent?: number
|
||||
period_discounts?: Record<number, number>
|
||||
auto_assign_total_spent_kopeks?: number | null
|
||||
apply_discounts_to_addons?: boolean
|
||||
is_default?: boolean
|
||||
name?: string;
|
||||
server_discount_percent?: number;
|
||||
traffic_discount_percent?: number;
|
||||
device_discount_percent?: number;
|
||||
period_discounts?: Record<number, number>;
|
||||
auto_assign_total_spent_kopeks?: number | null;
|
||||
apply_discounts_to_addons?: boolean;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
// ============== API ==============
|
||||
@@ -124,58 +129,58 @@ export interface PromoGroupUpdateRequest {
|
||||
export const promocodesApi = {
|
||||
// Promocodes
|
||||
getPromocodes: async (params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
is_active?: boolean
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
is_active?: boolean;
|
||||
}): Promise<PromoCodeListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/promocodes', { params })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/promocodes', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPromocode: async (id: number): Promise<PromoCodeDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/promocodes/${id}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/promocodes/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createPromocode: async (data: PromoCodeCreateRequest): Promise<PromoCode> => {
|
||||
const response = await apiClient.post('/cabinet/admin/promocodes', data)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/promocodes', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updatePromocode: async (id: number, data: PromoCodeUpdateRequest): Promise<PromoCode> => {
|
||||
const response = await apiClient.patch(`/cabinet/admin/promocodes/${id}`, data)
|
||||
return response.data
|
||||
const response = await apiClient.patch(`/cabinet/admin/promocodes/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deletePromocode: async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/promocodes/${id}`)
|
||||
await apiClient.delete(`/cabinet/admin/promocodes/${id}`);
|
||||
},
|
||||
|
||||
// Promo Groups
|
||||
getPromoGroups: async (params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<PromoGroupListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/promo-groups', { params })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/promo-groups', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPromoGroup: async (id: number): Promise<PromoGroup> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/promo-groups/${id}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/promo-groups/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createPromoGroup: async (data: PromoGroupCreateRequest): Promise<PromoGroup> => {
|
||||
const response = await apiClient.post('/cabinet/admin/promo-groups', data)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/promo-groups', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updatePromoGroup: async (id: number, data: PromoGroupUpdateRequest): Promise<PromoGroup> => {
|
||||
const response = await apiClient.patch(`/cabinet/admin/promo-groups/${id}`, data)
|
||||
return response.data
|
||||
const response = await apiClient.patch(`/cabinet/admin/promo-groups/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deletePromoGroup: async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/promo-groups/${id}`)
|
||||
await apiClient.delete(`/cabinet/admin/promo-groups/${id}`);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,62 +1,65 @@
|
||||
import apiClient from './client'
|
||||
import type { ReferralInfo, ReferralTerms, PaginatedResponse } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { ReferralInfo, ReferralTerms, PaginatedResponse } from '../types';
|
||||
|
||||
interface ReferralItem {
|
||||
id: number
|
||||
username: string | null
|
||||
first_name: string | null
|
||||
created_at: string
|
||||
has_subscription: boolean
|
||||
has_paid: boolean
|
||||
id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
created_at: string;
|
||||
has_subscription: boolean;
|
||||
has_paid: boolean;
|
||||
}
|
||||
|
||||
interface ReferralEarning {
|
||||
id: number
|
||||
amount_kopeks: number
|
||||
amount_rubles: number
|
||||
reason: string
|
||||
referral_username: string | null
|
||||
referral_first_name: string | null
|
||||
created_at: string
|
||||
id: number;
|
||||
amount_kopeks: number;
|
||||
amount_rubles: number;
|
||||
reason: string;
|
||||
referral_username: string | null;
|
||||
referral_first_name: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ReferralEarningsList extends PaginatedResponse<ReferralEarning> {
|
||||
total_amount_kopeks: number
|
||||
total_amount_rubles: number
|
||||
total_amount_kopeks: number;
|
||||
total_amount_rubles: number;
|
||||
}
|
||||
|
||||
export const referralApi = {
|
||||
// Get referral info
|
||||
getReferralInfo: async (): Promise<ReferralInfo> => {
|
||||
const response = await apiClient.get<ReferralInfo>('/cabinet/referral')
|
||||
return response.data
|
||||
const response = await apiClient.get<ReferralInfo>('/cabinet/referral');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get referral list
|
||||
getReferralList: async (params?: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}): Promise<PaginatedResponse<ReferralItem>> => {
|
||||
const response = await apiClient.get<PaginatedResponse<ReferralItem>>('/cabinet/referral/list', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.get<PaginatedResponse<ReferralItem>>(
|
||||
'/cabinet/referral/list',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get referral earnings
|
||||
getReferralEarnings: async (params?: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}): Promise<ReferralEarningsList> => {
|
||||
const response = await apiClient.get<ReferralEarningsList>('/cabinet/referral/earnings', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get referral terms
|
||||
getReferralTerms: async (): Promise<ReferralTerms> => {
|
||||
const response = await apiClient.get<ReferralTerms>('/cabinet/referral/terms')
|
||||
return response.data
|
||||
const response = await apiClient.get<ReferralTerms>('/cabinet/referral/terms');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,142 +1,142 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// Types
|
||||
export interface PromoGroupInfo {
|
||||
id: number
|
||||
name: string
|
||||
is_selected: boolean
|
||||
id: number;
|
||||
name: string;
|
||||
is_selected: boolean;
|
||||
}
|
||||
|
||||
export interface ServerListItem {
|
||||
id: number
|
||||
squad_uuid: string
|
||||
display_name: string
|
||||
original_name: string | null
|
||||
country_code: string | null
|
||||
is_available: boolean
|
||||
is_trial_eligible: boolean
|
||||
price_kopeks: number
|
||||
price_rubles: number
|
||||
max_users: number | null
|
||||
current_users: number
|
||||
sort_order: number
|
||||
is_full: boolean
|
||||
availability_status: string
|
||||
created_at: string
|
||||
id: number;
|
||||
squad_uuid: string;
|
||||
display_name: string;
|
||||
original_name: string | null;
|
||||
country_code: string | null;
|
||||
is_available: boolean;
|
||||
is_trial_eligible: boolean;
|
||||
price_kopeks: number;
|
||||
price_rubles: number;
|
||||
max_users: number | null;
|
||||
current_users: number;
|
||||
sort_order: number;
|
||||
is_full: boolean;
|
||||
availability_status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ServerListResponse {
|
||||
servers: ServerListItem[]
|
||||
total: number
|
||||
servers: ServerListItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ServerDetail {
|
||||
id: number
|
||||
squad_uuid: string
|
||||
display_name: string
|
||||
original_name: string | null
|
||||
country_code: string | null
|
||||
description: string | null
|
||||
is_available: boolean
|
||||
is_trial_eligible: boolean
|
||||
price_kopeks: number
|
||||
price_rubles: number
|
||||
max_users: number | null
|
||||
current_users: number
|
||||
sort_order: number
|
||||
is_full: boolean
|
||||
availability_status: string
|
||||
promo_groups: PromoGroupInfo[]
|
||||
active_subscriptions: number
|
||||
tariffs_using: string[]
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
id: number;
|
||||
squad_uuid: string;
|
||||
display_name: string;
|
||||
original_name: string | null;
|
||||
country_code: string | null;
|
||||
description: string | null;
|
||||
is_available: boolean;
|
||||
is_trial_eligible: boolean;
|
||||
price_kopeks: number;
|
||||
price_rubles: number;
|
||||
max_users: number | null;
|
||||
current_users: number;
|
||||
sort_order: number;
|
||||
is_full: boolean;
|
||||
availability_status: string;
|
||||
promo_groups: PromoGroupInfo[];
|
||||
active_subscriptions: number;
|
||||
tariffs_using: string[];
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface ServerUpdateRequest {
|
||||
display_name?: string
|
||||
description?: string
|
||||
country_code?: string
|
||||
is_available?: boolean
|
||||
is_trial_eligible?: boolean
|
||||
price_kopeks?: number
|
||||
max_users?: number
|
||||
sort_order?: number
|
||||
promo_group_ids?: number[]
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
country_code?: string;
|
||||
is_available?: boolean;
|
||||
is_trial_eligible?: boolean;
|
||||
price_kopeks?: number;
|
||||
max_users?: number;
|
||||
sort_order?: number;
|
||||
promo_group_ids?: number[];
|
||||
}
|
||||
|
||||
export interface ServerToggleResponse {
|
||||
id: number
|
||||
is_available: boolean
|
||||
message: string
|
||||
id: number;
|
||||
is_available: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ServerTrialToggleResponse {
|
||||
id: number
|
||||
is_trial_eligible: boolean
|
||||
message: string
|
||||
id: number;
|
||||
is_trial_eligible: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ServerStats {
|
||||
id: number
|
||||
display_name: string
|
||||
squad_uuid: string
|
||||
current_users: number
|
||||
max_users: number | null
|
||||
active_subscriptions: number
|
||||
trial_subscriptions: number
|
||||
usage_percent: number | null
|
||||
id: number;
|
||||
display_name: string;
|
||||
squad_uuid: string;
|
||||
current_users: number;
|
||||
max_users: number | null;
|
||||
active_subscriptions: number;
|
||||
trial_subscriptions: number;
|
||||
usage_percent: number | null;
|
||||
}
|
||||
|
||||
export interface ServerSyncResponse {
|
||||
created: number
|
||||
updated: number
|
||||
removed: number
|
||||
message: string
|
||||
created: number;
|
||||
updated: number;
|
||||
removed: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const serversApi = {
|
||||
// Get all servers
|
||||
getServers: async (includeUnavailable = true): Promise<ServerListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/servers', {
|
||||
params: { include_unavailable: includeUnavailable }
|
||||
})
|
||||
return response.data
|
||||
params: { include_unavailable: includeUnavailable },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single server
|
||||
getServer: async (serverId: number): Promise<ServerDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update server
|
||||
updateServer: async (serverId: number, data: ServerUpdateRequest): Promise<ServerDetail> => {
|
||||
const response = await apiClient.put(`/cabinet/admin/servers/${serverId}`, data)
|
||||
return response.data
|
||||
const response = await apiClient.put(`/cabinet/admin/servers/${serverId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle server availability
|
||||
toggleServer: async (serverId: number): Promise<ServerToggleResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/toggle`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle trial eligibility
|
||||
toggleTrial: async (serverId: number): Promise<ServerTrialToggleResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/trial`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/trial`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get server stats
|
||||
getServerStats: async (serverId: number): Promise<ServerStats> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}/stats`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Sync servers with RemnaWave
|
||||
syncServers: async (): Promise<ServerSyncResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/servers/sync')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/servers/sync');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,218 +1,256 @@
|
||||
import apiClient from './client'
|
||||
import type { Subscription, RenewalOption, TrafficPackage, TrialInfo, PurchaseOptions, PurchaseSelection, PurchasePreview, AppConfig } from '../types'
|
||||
import apiClient from './client';
|
||||
import type {
|
||||
Subscription,
|
||||
RenewalOption,
|
||||
TrafficPackage,
|
||||
TrialInfo,
|
||||
PurchaseOptions,
|
||||
PurchaseSelection,
|
||||
PurchasePreview,
|
||||
AppConfig,
|
||||
} from '../types';
|
||||
|
||||
export const subscriptionApi = {
|
||||
// Get current subscription
|
||||
getSubscription: async (): Promise<Subscription> => {
|
||||
const response = await apiClient.get<Subscription>('/cabinet/subscription')
|
||||
return response.data
|
||||
const response = await apiClient.get<Subscription>('/cabinet/subscription');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get renewal options
|
||||
getRenewalOptions: async (): Promise<RenewalOption[]> => {
|
||||
const response = await apiClient.get<RenewalOption[]>('/cabinet/subscription/renewal-options')
|
||||
return response.data
|
||||
const response = await apiClient.get<RenewalOption[]>('/cabinet/subscription/renewal-options');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Renew subscription
|
||||
renewSubscription: async (periodDays: number): Promise<{
|
||||
message: string
|
||||
new_end_date: string
|
||||
amount_paid_kopeks: number
|
||||
renewSubscription: async (
|
||||
periodDays: number,
|
||||
): Promise<{
|
||||
message: string;
|
||||
new_end_date: string;
|
||||
amount_paid_kopeks: number;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/renew', {
|
||||
period_days: periodDays,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get traffic packages
|
||||
getTrafficPackages: async (): Promise<TrafficPackage[]> => {
|
||||
const response = await apiClient.get<TrafficPackage[]>('/cabinet/subscription/traffic-packages')
|
||||
return response.data
|
||||
const response = await apiClient.get<TrafficPackage[]>(
|
||||
'/cabinet/subscription/traffic-packages',
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase traffic
|
||||
purchaseTraffic: async (gb: number): Promise<{
|
||||
message: string
|
||||
gb_added: number
|
||||
amount_paid_kopeks: number
|
||||
purchaseTraffic: async (
|
||||
gb: number,
|
||||
): Promise<{
|
||||
message: string;
|
||||
gb_added: number;
|
||||
amount_paid_kopeks: number;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/traffic', { gb })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/subscription/traffic', { gb });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase devices
|
||||
purchaseDevices: async (devices: number): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
devices_added: number
|
||||
new_device_limit: number
|
||||
price_kopeks: number
|
||||
price_label: string
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
purchaseDevices: async (
|
||||
devices: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
devices_added: number;
|
||||
new_device_limit: number;
|
||||
price_kopeks: number;
|
||||
price_label: string;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/devices/purchase', { devices })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/subscription/devices/purchase', { devices });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get device purchase price
|
||||
getDevicePrice: async (devices: number = 1): Promise<{
|
||||
available: boolean
|
||||
reason?: string
|
||||
devices?: number
|
||||
price_per_device_kopeks?: number
|
||||
price_per_device_label?: string
|
||||
total_price_kopeks?: number
|
||||
total_price_label?: string
|
||||
current_device_limit?: number
|
||||
days_left?: number
|
||||
base_device_price_kopeks?: number
|
||||
getDevicePrice: async (
|
||||
devices: number = 1,
|
||||
): Promise<{
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
devices?: number;
|
||||
price_per_device_kopeks?: number;
|
||||
price_per_device_label?: string;
|
||||
total_price_kopeks?: number;
|
||||
total_price_label?: string;
|
||||
current_device_limit?: number;
|
||||
days_left?: number;
|
||||
base_device_price_kopeks?: number;
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/subscription/devices/price', { params: { devices } })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/subscription/devices/price', {
|
||||
params: { devices },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update autopay settings
|
||||
updateAutopay: async (enabled: boolean, daysBefore?: number): Promise<{
|
||||
message: string
|
||||
autopay_enabled: boolean
|
||||
autopay_days_before: number
|
||||
updateAutopay: async (
|
||||
enabled: boolean,
|
||||
daysBefore?: number,
|
||||
): Promise<{
|
||||
message: string;
|
||||
autopay_enabled: boolean;
|
||||
autopay_days_before: number;
|
||||
}> => {
|
||||
const response = await apiClient.patch('/cabinet/subscription/autopay', {
|
||||
enabled,
|
||||
days_before: daysBefore,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get trial info
|
||||
getTrialInfo: async (): Promise<TrialInfo> => {
|
||||
const response = await apiClient.get<TrialInfo>('/cabinet/subscription/trial')
|
||||
return response.data
|
||||
const response = await apiClient.get<TrialInfo>('/cabinet/subscription/trial');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Activate trial
|
||||
activateTrial: async (): Promise<Subscription> => {
|
||||
const response = await apiClient.post<Subscription>('/cabinet/subscription/trial')
|
||||
return response.data
|
||||
const response = await apiClient.post<Subscription>('/cabinet/subscription/trial');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get purchase options (periods, servers, traffic, devices)
|
||||
getPurchaseOptions: async (): Promise<PurchaseOptions> => {
|
||||
const response = await apiClient.get<PurchaseOptions>('/cabinet/subscription/purchase-options')
|
||||
return response.data
|
||||
const response = await apiClient.get<PurchaseOptions>('/cabinet/subscription/purchase-options');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Preview purchase price
|
||||
previewPurchase: async (selection: PurchaseSelection): Promise<PurchasePreview> => {
|
||||
const response = await apiClient.post<PurchasePreview>('/cabinet/subscription/purchase-preview', {
|
||||
selection,
|
||||
})
|
||||
return response.data
|
||||
const response = await apiClient.post<PurchasePreview>(
|
||||
'/cabinet/subscription/purchase-preview',
|
||||
{
|
||||
selection,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Submit purchase
|
||||
submitPurchase: async (selection: PurchaseSelection): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
subscription: Subscription
|
||||
was_trial_conversion: boolean
|
||||
submitPurchase: async (
|
||||
selection: PurchaseSelection,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
subscription: Subscription;
|
||||
was_trial_conversion: boolean;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/purchase', {
|
||||
selection,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase tariff (for tariffs mode)
|
||||
purchaseTariff: async (tariffId: number, periodDays: number, trafficGb?: number): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
subscription: Subscription
|
||||
tariff_id: number
|
||||
tariff_name: string
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
purchaseTariff: async (
|
||||
tariffId: number,
|
||||
periodDays: number,
|
||||
trafficGb?: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
subscription: Subscription;
|
||||
tariff_id: number;
|
||||
tariff_name: string;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/purchase-tariff', {
|
||||
tariff_id: tariffId,
|
||||
period_days: periodDays,
|
||||
traffic_gb: trafficGb,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get app config for connection
|
||||
getAppConfig: async (): Promise<AppConfig> => {
|
||||
const response = await apiClient.get<AppConfig>('/cabinet/subscription/app-config')
|
||||
return response.data
|
||||
const response = await apiClient.get<AppConfig>('/cabinet/subscription/app-config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available countries/servers
|
||||
getCountries: async (): Promise<{
|
||||
countries: Array<{
|
||||
uuid: string
|
||||
name: string
|
||||
country_code: string | null
|
||||
base_price_kopeks: number
|
||||
price_kopeks: number
|
||||
price_per_month_kopeks: number
|
||||
price_rubles: number
|
||||
is_available: boolean
|
||||
is_connected: boolean
|
||||
has_discount: boolean
|
||||
discount_percent: number
|
||||
}>
|
||||
connected_count: number
|
||||
has_subscription: boolean
|
||||
days_left: number
|
||||
discount_percent: number
|
||||
uuid: string;
|
||||
name: string;
|
||||
country_code: string | null;
|
||||
base_price_kopeks: number;
|
||||
price_kopeks: number;
|
||||
price_per_month_kopeks: number;
|
||||
price_rubles: number;
|
||||
is_available: boolean;
|
||||
is_connected: boolean;
|
||||
has_discount: boolean;
|
||||
discount_percent: number;
|
||||
}>;
|
||||
connected_count: number;
|
||||
has_subscription: boolean;
|
||||
days_left: number;
|
||||
discount_percent: number;
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/subscription/countries')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/subscription/countries');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update countries/servers
|
||||
updateCountries: async (countries: string[]): Promise<{
|
||||
message: string
|
||||
added: string[]
|
||||
removed: string[]
|
||||
amount_paid_kopeks: number
|
||||
connected_squads: string[]
|
||||
updateCountries: async (
|
||||
countries: string[],
|
||||
): Promise<{
|
||||
message: string;
|
||||
added: string[];
|
||||
removed: string[];
|
||||
amount_paid_kopeks: number;
|
||||
connected_squads: string[];
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/countries', { countries })
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/subscription/countries', { countries });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get connection link and instructions
|
||||
getConnectionLink: async (): Promise<{
|
||||
subscription_url: string | null
|
||||
display_link: string | null
|
||||
happ_redirect_link: string | null
|
||||
happ_scheme_link: string | null
|
||||
connect_mode: string
|
||||
hide_link: boolean
|
||||
subscription_url: string | null;
|
||||
display_link: string | null;
|
||||
happ_redirect_link: string | null;
|
||||
happ_scheme_link: string | null;
|
||||
connect_mode: string;
|
||||
hide_link: boolean;
|
||||
instructions: {
|
||||
steps: string[]
|
||||
}
|
||||
steps: string[];
|
||||
};
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/subscription/connection-link')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/subscription/connection-link');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get hApp download links
|
||||
getHappDownloads: async (): Promise<{
|
||||
platforms: Record<string, {
|
||||
name: string
|
||||
icon: string
|
||||
link: string
|
||||
}>
|
||||
happ_enabled: boolean
|
||||
platforms: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
icon: string;
|
||||
link: string;
|
||||
}
|
||||
>;
|
||||
happ_enabled: boolean;
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/subscription/happ-downloads')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/subscription/happ-downloads');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Device Management ============
|
||||
@@ -220,130 +258,140 @@ export const subscriptionApi = {
|
||||
// Get connected devices
|
||||
getDevices: async (): Promise<{
|
||||
devices: Array<{
|
||||
hwid: string
|
||||
platform: string
|
||||
device_model: string
|
||||
created_at: string | null
|
||||
}>
|
||||
total: number
|
||||
device_limit: number
|
||||
hwid: string;
|
||||
platform: string;
|
||||
device_model: string;
|
||||
created_at: string | null;
|
||||
}>;
|
||||
total: number;
|
||||
device_limit: number;
|
||||
}> => {
|
||||
const response = await apiClient.get('/cabinet/subscription/devices')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/subscription/devices');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete a specific device
|
||||
deleteDevice: async (hwid: string): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
deleted_hwid: string
|
||||
deleteDevice: async (
|
||||
hwid: string,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
deleted_hwid: string;
|
||||
}> => {
|
||||
const response = await apiClient.delete(`/cabinet/subscription/devices/${encodeURIComponent(hwid)}`)
|
||||
return response.data
|
||||
const response = await apiClient.delete(
|
||||
`/cabinet/subscription/devices/${encodeURIComponent(hwid)}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete all devices
|
||||
deleteAllDevices: async (): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
deleted_count: number
|
||||
success: boolean;
|
||||
message: string;
|
||||
deleted_count: number;
|
||||
}> => {
|
||||
const response = await apiClient.delete('/cabinet/subscription/devices')
|
||||
return response.data
|
||||
const response = await apiClient.delete('/cabinet/subscription/devices');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Tariff Switch ============
|
||||
|
||||
// Preview tariff switch cost
|
||||
previewTariffSwitch: async (tariffId: number): Promise<{
|
||||
can_switch: boolean
|
||||
current_tariff_id: number | null
|
||||
current_tariff_name: string | null
|
||||
new_tariff_id: number
|
||||
new_tariff_name: string
|
||||
remaining_days: number
|
||||
upgrade_cost_kopeks: number
|
||||
upgrade_cost_label: string
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
has_enough_balance: boolean
|
||||
missing_amount_kopeks: number
|
||||
missing_amount_label: string
|
||||
is_upgrade: boolean
|
||||
previewTariffSwitch: async (
|
||||
tariffId: number,
|
||||
): Promise<{
|
||||
can_switch: boolean;
|
||||
current_tariff_id: number | null;
|
||||
current_tariff_name: string | null;
|
||||
new_tariff_id: number;
|
||||
new_tariff_name: string;
|
||||
remaining_days: number;
|
||||
upgrade_cost_kopeks: number;
|
||||
upgrade_cost_label: string;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
has_enough_balance: boolean;
|
||||
missing_amount_kopeks: number;
|
||||
missing_amount_label: string;
|
||||
is_upgrade: boolean;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/tariff/switch/preview', {
|
||||
tariff_id: tariffId,
|
||||
period_days: 30, // Default period for switch
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Switch to a different tariff
|
||||
switchTariff: async (tariffId: number): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
subscription: Subscription
|
||||
old_tariff_name: string
|
||||
new_tariff_id: number
|
||||
new_tariff_name: string
|
||||
charged_kopeks: number
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
switchTariff: async (
|
||||
tariffId: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
subscription: Subscription;
|
||||
old_tariff_name: string;
|
||||
new_tariff_id: number;
|
||||
new_tariff_name: string;
|
||||
charged_kopeks: number;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/tariff/switch', {
|
||||
tariff_id: tariffId,
|
||||
period_days: 30,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Subscription Pause (Daily Tariffs) ============
|
||||
|
||||
// Toggle pause/resume for daily subscription
|
||||
togglePause: async (): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
is_paused: boolean
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
success: boolean;
|
||||
message: string;
|
||||
is_paused: boolean;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/pause')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/subscription/pause');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Traffic Switch ============
|
||||
|
||||
// Switch to a different traffic package
|
||||
switchTraffic: async (gb: number): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
old_traffic_gb: number
|
||||
new_traffic_gb: number
|
||||
charged_kopeks: number
|
||||
balance_kopeks: number
|
||||
balance_label: string
|
||||
switchTraffic: async (
|
||||
gb: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
old_traffic_gb: number;
|
||||
new_traffic_gb: number;
|
||||
charged_kopeks: number;
|
||||
balance_kopeks: number;
|
||||
balance_label: string;
|
||||
}> => {
|
||||
const response = await apiClient.put('/cabinet/subscription/traffic', { gb })
|
||||
return response.data
|
||||
const response = await apiClient.put('/cabinet/subscription/traffic', { gb });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Refresh traffic usage from RemnaWave (rate limited: 1 per 60 seconds)
|
||||
refreshTraffic: async (): Promise<{
|
||||
success: boolean
|
||||
cached: boolean
|
||||
rate_limited?: boolean
|
||||
retry_after_seconds?: number
|
||||
source?: string
|
||||
traffic_used_bytes: number
|
||||
traffic_used_gb: number
|
||||
traffic_limit_bytes: number
|
||||
traffic_limit_gb: number
|
||||
traffic_used_percent: number
|
||||
is_unlimited: boolean
|
||||
lifetime_used_bytes?: number
|
||||
lifetime_used_gb?: number
|
||||
success: boolean;
|
||||
cached: boolean;
|
||||
rate_limited?: boolean;
|
||||
retry_after_seconds?: number;
|
||||
source?: string;
|
||||
traffic_used_bytes: number;
|
||||
traffic_used_gb: number;
|
||||
traffic_limit_bytes: number;
|
||||
traffic_limit_gb: number;
|
||||
traffic_used_percent: number;
|
||||
is_unlimited: boolean;
|
||||
lifetime_used_bytes?: number;
|
||||
lifetime_used_gb?: number;
|
||||
}> => {
|
||||
const response = await apiClient.post('/cabinet/subscription/refresh-traffic')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/subscription/refresh-traffic');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,239 +1,239 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// Types
|
||||
export interface PeriodPrice {
|
||||
days: number
|
||||
price_kopeks: number
|
||||
price_rubles?: number
|
||||
days: number;
|
||||
price_kopeks: number;
|
||||
price_rubles?: number;
|
||||
}
|
||||
|
||||
export interface ServerTrafficLimit {
|
||||
traffic_limit_gb: number
|
||||
traffic_limit_gb: number;
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
id: number
|
||||
squad_uuid: string
|
||||
display_name: string
|
||||
country_code: string | null
|
||||
is_selected: boolean
|
||||
traffic_limit_gb?: number | null
|
||||
id: number;
|
||||
squad_uuid: string;
|
||||
display_name: string;
|
||||
country_code: string | null;
|
||||
is_selected: boolean;
|
||||
traffic_limit_gb?: number | null;
|
||||
}
|
||||
|
||||
export interface PromoGroupInfo {
|
||||
id: number
|
||||
name: string
|
||||
is_selected: boolean
|
||||
id: number;
|
||||
name: string;
|
||||
is_selected: boolean;
|
||||
}
|
||||
|
||||
export interface TariffListItem {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
is_trial_available: boolean
|
||||
is_daily: boolean
|
||||
daily_price_kopeks: number
|
||||
traffic_limit_gb: number
|
||||
device_limit: number
|
||||
tier_level: number
|
||||
display_order: number
|
||||
servers_count: number
|
||||
subscriptions_count: number
|
||||
created_at: string
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
is_active: boolean;
|
||||
is_trial_available: boolean;
|
||||
is_daily: boolean;
|
||||
daily_price_kopeks: number;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
tier_level: number;
|
||||
display_order: number;
|
||||
servers_count: number;
|
||||
subscriptions_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TariffListResponse {
|
||||
tariffs: TariffListItem[]
|
||||
total: number
|
||||
tariffs: TariffListItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TariffDetail {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
is_trial_available: boolean
|
||||
traffic_limit_gb: number
|
||||
device_limit: number
|
||||
device_price_kopeks: number | null
|
||||
max_device_limit: number | null
|
||||
tier_level: number
|
||||
display_order: number
|
||||
period_prices: PeriodPrice[]
|
||||
allowed_squads: string[]
|
||||
server_traffic_limits: Record<string, ServerTrafficLimit>
|
||||
servers: ServerInfo[]
|
||||
promo_groups: PromoGroupInfo[]
|
||||
subscriptions_count: number
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
is_active: boolean;
|
||||
is_trial_available: boolean;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
device_price_kopeks: number | null;
|
||||
max_device_limit: number | null;
|
||||
tier_level: number;
|
||||
display_order: number;
|
||||
period_prices: PeriodPrice[];
|
||||
allowed_squads: string[];
|
||||
server_traffic_limits: Record<string, ServerTrafficLimit>;
|
||||
servers: ServerInfo[];
|
||||
promo_groups: PromoGroupInfo[];
|
||||
subscriptions_count: number;
|
||||
// Произвольное количество дней
|
||||
custom_days_enabled: boolean
|
||||
price_per_day_kopeks: number
|
||||
min_days: number
|
||||
max_days: number
|
||||
custom_days_enabled: boolean;
|
||||
price_per_day_kopeks: number;
|
||||
min_days: number;
|
||||
max_days: number;
|
||||
// Произвольный трафик при покупке
|
||||
custom_traffic_enabled: boolean
|
||||
traffic_price_per_gb_kopeks: number
|
||||
min_traffic_gb: number
|
||||
max_traffic_gb: number
|
||||
custom_traffic_enabled: boolean;
|
||||
traffic_price_per_gb_kopeks: number;
|
||||
min_traffic_gb: number;
|
||||
max_traffic_gb: number;
|
||||
// Докупка трафика
|
||||
traffic_topup_enabled: boolean
|
||||
traffic_topup_packages: Record<string, number>
|
||||
max_topup_traffic_gb: number
|
||||
traffic_topup_enabled: boolean;
|
||||
traffic_topup_packages: Record<string, number>;
|
||||
max_topup_traffic_gb: number;
|
||||
// Дневной тариф
|
||||
is_daily: boolean
|
||||
daily_price_kopeks: number
|
||||
is_daily: boolean;
|
||||
daily_price_kopeks: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode: string | null // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface TariffCreateRequest {
|
||||
name: string
|
||||
description?: string
|
||||
is_active?: boolean
|
||||
traffic_limit_gb?: number
|
||||
device_limit?: number
|
||||
device_price_kopeks?: number
|
||||
max_device_limit?: number
|
||||
tier_level?: number
|
||||
period_prices?: PeriodPrice[]
|
||||
allowed_squads?: string[]
|
||||
server_traffic_limits?: Record<string, ServerTrafficLimit>
|
||||
promo_group_ids?: number[]
|
||||
name: string;
|
||||
description?: string;
|
||||
is_active?: boolean;
|
||||
traffic_limit_gb?: number;
|
||||
device_limit?: number;
|
||||
device_price_kopeks?: number;
|
||||
max_device_limit?: number;
|
||||
tier_level?: number;
|
||||
period_prices?: PeriodPrice[];
|
||||
allowed_squads?: string[];
|
||||
server_traffic_limits?: Record<string, ServerTrafficLimit>;
|
||||
promo_group_ids?: number[];
|
||||
// Произвольное количество дней
|
||||
custom_days_enabled?: boolean
|
||||
price_per_day_kopeks?: number
|
||||
min_days?: number
|
||||
max_days?: number
|
||||
custom_days_enabled?: boolean;
|
||||
price_per_day_kopeks?: number;
|
||||
min_days?: number;
|
||||
max_days?: number;
|
||||
// Произвольный трафик при покупке
|
||||
custom_traffic_enabled?: boolean
|
||||
traffic_price_per_gb_kopeks?: number
|
||||
min_traffic_gb?: number
|
||||
max_traffic_gb?: number
|
||||
custom_traffic_enabled?: boolean;
|
||||
traffic_price_per_gb_kopeks?: number;
|
||||
min_traffic_gb?: number;
|
||||
max_traffic_gb?: number;
|
||||
// Докупка трафика
|
||||
traffic_topup_enabled?: boolean
|
||||
traffic_topup_packages?: Record<string, number>
|
||||
max_topup_traffic_gb?: number
|
||||
traffic_topup_enabled?: boolean;
|
||||
traffic_topup_packages?: Record<string, number>;
|
||||
max_topup_traffic_gb?: number;
|
||||
// Дневной тариф
|
||||
is_daily?: boolean
|
||||
daily_price_kopeks?: number
|
||||
is_daily?: boolean;
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null
|
||||
traffic_reset_mode?: string | null;
|
||||
}
|
||||
|
||||
export interface TariffUpdateRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
is_active?: boolean
|
||||
traffic_limit_gb?: number
|
||||
device_limit?: number
|
||||
device_price_kopeks?: number
|
||||
max_device_limit?: number
|
||||
tier_level?: number
|
||||
display_order?: number
|
||||
period_prices?: PeriodPrice[]
|
||||
allowed_squads?: string[]
|
||||
server_traffic_limits?: Record<string, ServerTrafficLimit>
|
||||
promo_group_ids?: number[]
|
||||
name?: string;
|
||||
description?: string;
|
||||
is_active?: boolean;
|
||||
traffic_limit_gb?: number;
|
||||
device_limit?: number;
|
||||
device_price_kopeks?: number;
|
||||
max_device_limit?: number;
|
||||
tier_level?: number;
|
||||
display_order?: number;
|
||||
period_prices?: PeriodPrice[];
|
||||
allowed_squads?: string[];
|
||||
server_traffic_limits?: Record<string, ServerTrafficLimit>;
|
||||
promo_group_ids?: number[];
|
||||
// Произвольное количество дней
|
||||
custom_days_enabled?: boolean
|
||||
price_per_day_kopeks?: number
|
||||
min_days?: number
|
||||
max_days?: number
|
||||
custom_days_enabled?: boolean;
|
||||
price_per_day_kopeks?: number;
|
||||
min_days?: number;
|
||||
max_days?: number;
|
||||
// Произвольный трафик при покупке
|
||||
custom_traffic_enabled?: boolean
|
||||
traffic_price_per_gb_kopeks?: number
|
||||
min_traffic_gb?: number
|
||||
max_traffic_gb?: number
|
||||
custom_traffic_enabled?: boolean;
|
||||
traffic_price_per_gb_kopeks?: number;
|
||||
min_traffic_gb?: number;
|
||||
max_traffic_gb?: number;
|
||||
// Докупка трафика
|
||||
traffic_topup_enabled?: boolean
|
||||
traffic_topup_packages?: Record<string, number>
|
||||
max_topup_traffic_gb?: number
|
||||
traffic_topup_enabled?: boolean;
|
||||
traffic_topup_packages?: Record<string, number>;
|
||||
max_topup_traffic_gb?: number;
|
||||
// Дневной тариф
|
||||
is_daily?: boolean
|
||||
daily_price_kopeks?: number
|
||||
is_daily?: boolean;
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null
|
||||
traffic_reset_mode?: string | null;
|
||||
}
|
||||
|
||||
export interface TariffToggleResponse {
|
||||
id: number
|
||||
is_active: boolean
|
||||
message: string
|
||||
id: number;
|
||||
is_active: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TariffTrialResponse {
|
||||
id: number
|
||||
is_trial_available: boolean
|
||||
message: string
|
||||
id: number;
|
||||
is_trial_available: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TariffStats {
|
||||
id: number
|
||||
name: string
|
||||
subscriptions_count: number
|
||||
active_subscriptions: number
|
||||
trial_subscriptions: number
|
||||
revenue_kopeks: number
|
||||
revenue_rubles: number
|
||||
id: number;
|
||||
name: string;
|
||||
subscriptions_count: number;
|
||||
active_subscriptions: number;
|
||||
trial_subscriptions: number;
|
||||
revenue_kopeks: number;
|
||||
revenue_rubles: number;
|
||||
}
|
||||
|
||||
export const tariffsApi = {
|
||||
// Get all tariffs
|
||||
getTariffs: async (includeInactive = true): Promise<TariffListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tariffs', {
|
||||
params: { include_inactive: includeInactive }
|
||||
})
|
||||
return response.data
|
||||
params: { include_inactive: includeInactive },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get single tariff
|
||||
getTariff: async (tariffId: number): Promise<TariffDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create new tariff
|
||||
createTariff: async (data: TariffCreateRequest): Promise<TariffDetail> => {
|
||||
const response = await apiClient.post('/cabinet/admin/tariffs', data)
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/tariffs', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update tariff
|
||||
updateTariff: async (tariffId: number, data: TariffUpdateRequest): Promise<TariffDetail> => {
|
||||
const response = await apiClient.put(`/cabinet/admin/tariffs/${tariffId}`, data)
|
||||
return response.data
|
||||
const response = await apiClient.put(`/cabinet/admin/tariffs/${tariffId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete tariff
|
||||
deleteTariff: async (tariffId: number): Promise<{ message: string }> => {
|
||||
const response = await apiClient.delete(`/cabinet/admin/tariffs/${tariffId}`)
|
||||
return response.data
|
||||
const response = await apiClient.delete(`/cabinet/admin/tariffs/${tariffId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle tariff active status
|
||||
toggleTariff: async (tariffId: number): Promise<TariffToggleResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/toggle`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Toggle trial status
|
||||
toggleTrial: async (tariffId: number): Promise<TariffTrialResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/trial`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/trial`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get tariff stats
|
||||
getTariffStats: async (tariffId: number): Promise<TariffStats> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}/stats`)
|
||||
return response.data
|
||||
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available servers for selection
|
||||
getAvailableServers: async (): Promise<ServerInfo[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tariffs/available-servers')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/tariffs/available-servers');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
import apiClient from './client'
|
||||
import { ThemeSettings, DEFAULT_THEME_COLORS, EnabledThemes, DEFAULT_ENABLED_THEMES } from '../types/theme'
|
||||
import apiClient from './client';
|
||||
import {
|
||||
ThemeSettings,
|
||||
DEFAULT_THEME_COLORS,
|
||||
EnabledThemes,
|
||||
DEFAULT_ENABLED_THEMES,
|
||||
} from '../types/theme';
|
||||
|
||||
export const themeColorsApi = {
|
||||
// Get current theme colors (public, no auth required)
|
||||
getColors: async (): Promise<ThemeSettings> => {
|
||||
try {
|
||||
const response = await apiClient.get<ThemeSettings>('/cabinet/branding/colors')
|
||||
return response.data
|
||||
const response = await apiClient.get<ThemeSettings>('/cabinet/branding/colors');
|
||||
return response.data;
|
||||
} catch {
|
||||
// Return default colors if endpoint not available
|
||||
return DEFAULT_THEME_COLORS
|
||||
return DEFAULT_THEME_COLORS;
|
||||
}
|
||||
},
|
||||
|
||||
// Update theme colors (admin only)
|
||||
updateColors: async (colors: Partial<ThemeSettings>): Promise<ThemeSettings> => {
|
||||
const response = await apiClient.patch<ThemeSettings>('/cabinet/branding/colors', colors)
|
||||
return response.data
|
||||
const response = await apiClient.patch<ThemeSettings>('/cabinet/branding/colors', colors);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reset to default colors (admin only)
|
||||
resetColors: async (): Promise<ThemeSettings> => {
|
||||
const response = await apiClient.post<ThemeSettings>('/cabinet/branding/colors/reset')
|
||||
return response.data
|
||||
const response = await apiClient.post<ThemeSettings>('/cabinet/branding/colors/reset');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get enabled themes (public, no auth required)
|
||||
getEnabledThemes: async (): Promise<EnabledThemes> => {
|
||||
try {
|
||||
const response = await apiClient.get<EnabledThemes>('/cabinet/branding/themes')
|
||||
return response.data
|
||||
const response = await apiClient.get<EnabledThemes>('/cabinet/branding/themes');
|
||||
return response.data;
|
||||
} catch {
|
||||
return DEFAULT_ENABLED_THEMES
|
||||
return DEFAULT_ENABLED_THEMES;
|
||||
}
|
||||
},
|
||||
|
||||
// Update enabled themes (admin only)
|
||||
updateEnabledThemes: async (themes: Partial<EnabledThemes>): Promise<EnabledThemes> => {
|
||||
const response = await apiClient.patch<EnabledThemes>('/cabinet/branding/themes', themes)
|
||||
return response.data
|
||||
const response = await apiClient.patch<EnabledThemes>('/cabinet/branding/themes', themes);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,64 +1,80 @@
|
||||
import apiClient from './client'
|
||||
import type { TicketNotificationList, UnreadCountResponse } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { TicketNotificationList, UnreadCountResponse } from '../types';
|
||||
|
||||
export const ticketNotificationsApi = {
|
||||
// User notifications
|
||||
getNotifications: async (unreadOnly = false, limit = 50, offset = 0): Promise<TicketNotificationList> => {
|
||||
getNotifications: async (
|
||||
unreadOnly = false,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
): Promise<TicketNotificationList> => {
|
||||
const response = await apiClient.get('/cabinet/tickets/notifications', {
|
||||
params: { unread_only: unreadOnly, limit, offset }
|
||||
})
|
||||
return response.data
|
||||
params: { unread_only: unreadOnly, limit, offset },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getUnreadCount: async (): Promise<UnreadCountResponse> => {
|
||||
const response = await apiClient.get('/cabinet/tickets/notifications/unread-count')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/tickets/notifications/unread-count');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAsRead: async (notificationId: number): Promise<{ success: boolean }> => {
|
||||
const response = await apiClient.post(`/cabinet/tickets/notifications/${notificationId}/read`)
|
||||
return response.data
|
||||
const response = await apiClient.post(`/cabinet/tickets/notifications/${notificationId}/read`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAllAsRead: async (): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post('/cabinet/tickets/notifications/read-all')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/tickets/notifications/read-all');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markTicketAsRead: async (ticketId: number): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post(`/cabinet/tickets/notifications/ticket/${ticketId}/read`)
|
||||
return response.data
|
||||
markTicketAsRead: async (
|
||||
ticketId: number,
|
||||
): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post(`/cabinet/tickets/notifications/ticket/${ticketId}/read`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Admin notifications
|
||||
getAdminNotifications: async (unreadOnly = false, limit = 50, offset = 0): Promise<TicketNotificationList> => {
|
||||
const params: Record<string, unknown> = { limit, offset }
|
||||
getAdminNotifications: async (
|
||||
unreadOnly = false,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
): Promise<TicketNotificationList> => {
|
||||
const params: Record<string, unknown> = { limit, offset };
|
||||
if (unreadOnly) {
|
||||
params.unread_only = true
|
||||
params.unread_only = true;
|
||||
}
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/notifications', { params })
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/notifications', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getAdminUnreadCount: async (): Promise<UnreadCountResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/notifications/unread-count')
|
||||
return response.data
|
||||
const response = await apiClient.get('/cabinet/admin/tickets/notifications/unread-count');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAdminAsRead: async (notificationId: number): Promise<{ success: boolean }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/notifications/${notificationId}/read`)
|
||||
return response.data
|
||||
const response = await apiClient.post(
|
||||
`/cabinet/admin/tickets/notifications/${notificationId}/read`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAllAdminAsRead: async (): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post('/cabinet/admin/tickets/notifications/read-all')
|
||||
return response.data
|
||||
const response = await apiClient.post('/cabinet/admin/tickets/notifications/read-all');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
markAdminTicketAsRead: async (ticketId: number): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/tickets/notifications/ticket/${ticketId}/read`)
|
||||
return response.data
|
||||
markAdminTicketAsRead: async (
|
||||
ticketId: number,
|
||||
): Promise<{ success: boolean; marked_count: number }> => {
|
||||
const response = await apiClient.post(
|
||||
`/cabinet/admin/tickets/notifications/ticket/${ticketId}/read`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default ticketNotificationsApi
|
||||
export default ticketNotificationsApi;
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
import apiClient from './client'
|
||||
import type { Ticket, TicketDetail, TicketMessage, PaginatedResponse } from '../types'
|
||||
import apiClient from './client';
|
||||
import type { Ticket, TicketDetail, TicketMessage, PaginatedResponse } from '../types';
|
||||
|
||||
// Media upload response type
|
||||
interface MediaUploadResponse {
|
||||
media_type: string
|
||||
file_id: string
|
||||
file_unique_id: string | null
|
||||
media_url: string
|
||||
media_type: string;
|
||||
file_id: string;
|
||||
file_unique_id: string | null;
|
||||
media_url: string;
|
||||
}
|
||||
|
||||
// Media parameters for ticket messages
|
||||
interface MediaParams {
|
||||
media_type?: string
|
||||
media_file_id?: string
|
||||
media_caption?: string
|
||||
media_type?: string;
|
||||
media_file_id?: string;
|
||||
media_caption?: string;
|
||||
}
|
||||
|
||||
export const ticketsApi = {
|
||||
// Get tickets list
|
||||
getTickets: async (params?: {
|
||||
page?: number
|
||||
per_page?: number
|
||||
status?: string
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
status?: string;
|
||||
}): Promise<PaginatedResponse<Ticket>> => {
|
||||
const response = await apiClient.get<PaginatedResponse<Ticket>>('/cabinet/tickets', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create ticket with optional media
|
||||
createTicket: async (
|
||||
title: string,
|
||||
message: string,
|
||||
media?: MediaParams
|
||||
media?: MediaParams,
|
||||
): Promise<TicketDetail> => {
|
||||
const response = await apiClient.post<TicketDetail>('/cabinet/tickets', {
|
||||
title,
|
||||
message,
|
||||
...media,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get ticket detail
|
||||
getTicket: async (ticketId: number): Promise<TicketDetail> => {
|
||||
const response = await apiClient.get<TicketDetail>(`/cabinet/tickets/${ticketId}`)
|
||||
return response.data
|
||||
const response = await apiClient.get<TicketDetail>(`/cabinet/tickets/${ticketId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Add message to ticket with optional media
|
||||
addMessage: async (
|
||||
ticketId: number,
|
||||
message: string,
|
||||
media?: MediaParams
|
||||
media?: MediaParams,
|
||||
): Promise<TicketMessage> => {
|
||||
const response = await apiClient.post<TicketMessage>(`/cabinet/tickets/${ticketId}/messages`, {
|
||||
message,
|
||||
...media,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Upload media file for tickets
|
||||
uploadMedia: async (file: File, mediaType: string = 'photo'): Promise<MediaUploadResponse> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('media_type', mediaType)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('media_type', mediaType);
|
||||
|
||||
const response = await apiClient.post<MediaUploadResponse>('/cabinet/media/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get media URL for display
|
||||
getMediaUrl: (fileId: string): string => {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
return `${baseUrl}/cabinet/media/${fileId}`
|
||||
const baseUrl = import.meta.env.VITE_API_URL || '';
|
||||
return `${baseUrl}/cabinet/media/${fileId}`;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
344
src/api/wheel.ts
344
src/api/wheel.ts
@@ -1,182 +1,182 @@
|
||||
import apiClient from './client'
|
||||
import apiClient from './client';
|
||||
|
||||
// ==================== TYPES ====================
|
||||
|
||||
export interface WheelPrize {
|
||||
id: number
|
||||
display_name: string
|
||||
emoji: string
|
||||
color: string
|
||||
prize_type: string
|
||||
id: number;
|
||||
display_name: string;
|
||||
emoji: string;
|
||||
color: string;
|
||||
prize_type: string;
|
||||
}
|
||||
|
||||
export interface WheelConfig {
|
||||
is_enabled: boolean
|
||||
name: string
|
||||
spin_cost_stars: number | null
|
||||
spin_cost_days: number | null
|
||||
spin_cost_stars_enabled: boolean
|
||||
spin_cost_days_enabled: boolean
|
||||
prizes: WheelPrize[]
|
||||
daily_limit: number
|
||||
user_spins_today: number
|
||||
can_spin: boolean
|
||||
can_spin_reason: string | null
|
||||
can_pay_stars: boolean
|
||||
can_pay_days: boolean
|
||||
user_balance_kopeks: number
|
||||
required_balance_kopeks: number
|
||||
is_enabled: boolean;
|
||||
name: string;
|
||||
spin_cost_stars: number | null;
|
||||
spin_cost_days: number | null;
|
||||
spin_cost_stars_enabled: boolean;
|
||||
spin_cost_days_enabled: boolean;
|
||||
prizes: WheelPrize[];
|
||||
daily_limit: number;
|
||||
user_spins_today: number;
|
||||
can_spin: boolean;
|
||||
can_spin_reason: string | null;
|
||||
can_pay_stars: boolean;
|
||||
can_pay_days: boolean;
|
||||
user_balance_kopeks: number;
|
||||
required_balance_kopeks: number;
|
||||
}
|
||||
|
||||
export interface SpinAvailability {
|
||||
can_spin: boolean
|
||||
reason: string | null
|
||||
spins_remaining_today: number
|
||||
can_pay_stars: boolean
|
||||
can_pay_days: boolean
|
||||
min_subscription_days: number
|
||||
user_subscription_days: number
|
||||
can_spin: boolean;
|
||||
reason: string | null;
|
||||
spins_remaining_today: number;
|
||||
can_pay_stars: boolean;
|
||||
can_pay_days: boolean;
|
||||
min_subscription_days: number;
|
||||
user_subscription_days: number;
|
||||
}
|
||||
|
||||
export interface SpinResult {
|
||||
success: boolean
|
||||
prize_id: number | null
|
||||
prize_type: string | null
|
||||
prize_value: number
|
||||
prize_display_name: string
|
||||
emoji: string
|
||||
color: string
|
||||
rotation_degrees: number
|
||||
message: string
|
||||
promocode: string | null
|
||||
error: string | null
|
||||
success: boolean;
|
||||
prize_id: number | null;
|
||||
prize_type: string | null;
|
||||
prize_value: number;
|
||||
prize_display_name: string;
|
||||
emoji: string;
|
||||
color: string;
|
||||
rotation_degrees: number;
|
||||
message: string;
|
||||
promocode: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface SpinHistoryItem {
|
||||
id: number
|
||||
payment_type: string
|
||||
payment_amount: number
|
||||
prize_type: string
|
||||
prize_value: number
|
||||
prize_display_name: string
|
||||
emoji: string
|
||||
color: string
|
||||
prize_value_kopeks: number
|
||||
created_at: string
|
||||
id: number;
|
||||
payment_type: string;
|
||||
payment_amount: number;
|
||||
prize_type: string;
|
||||
prize_value: number;
|
||||
prize_display_name: string;
|
||||
emoji: string;
|
||||
color: string;
|
||||
prize_value_kopeks: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface SpinHistoryResponse {
|
||||
items: SpinHistoryItem[]
|
||||
total: number
|
||||
page: number
|
||||
per_page: number
|
||||
pages: number
|
||||
items: SpinHistoryItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface StarsInvoiceResponse {
|
||||
invoice_url: string
|
||||
stars_amount: number
|
||||
invoice_url: string;
|
||||
stars_amount: number;
|
||||
}
|
||||
|
||||
// Admin types
|
||||
export interface WheelPrizeAdmin {
|
||||
id: number
|
||||
config_id: number
|
||||
prize_type: string
|
||||
prize_value: number
|
||||
display_name: string
|
||||
emoji: string
|
||||
color: string
|
||||
prize_value_kopeks: number
|
||||
sort_order: number
|
||||
manual_probability: number | null
|
||||
is_active: boolean
|
||||
promo_balance_bonus_kopeks: number
|
||||
promo_subscription_days: number
|
||||
promo_traffic_gb: number
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
id: number;
|
||||
config_id: number;
|
||||
prize_type: string;
|
||||
prize_value: number;
|
||||
display_name: string;
|
||||
emoji: string;
|
||||
color: string;
|
||||
prize_value_kopeks: number;
|
||||
sort_order: number;
|
||||
manual_probability: number | null;
|
||||
is_active: boolean;
|
||||
promo_balance_bonus_kopeks: number;
|
||||
promo_subscription_days: number;
|
||||
promo_traffic_gb: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
// Type for creating a new prize (excludes id, config_id which are auto-generated)
|
||||
export interface CreateWheelPrizeData {
|
||||
prize_type: string
|
||||
prize_value: number
|
||||
display_name: string
|
||||
emoji?: string
|
||||
color?: string
|
||||
prize_value_kopeks: number
|
||||
sort_order?: number
|
||||
manual_probability?: number | null
|
||||
is_active?: boolean
|
||||
promo_balance_bonus_kopeks?: number
|
||||
promo_subscription_days?: number
|
||||
promo_traffic_gb?: number
|
||||
prize_type: string;
|
||||
prize_value: number;
|
||||
display_name: string;
|
||||
emoji?: string;
|
||||
color?: string;
|
||||
prize_value_kopeks: number;
|
||||
sort_order?: number;
|
||||
manual_probability?: number | null;
|
||||
is_active?: boolean;
|
||||
promo_balance_bonus_kopeks?: number;
|
||||
promo_subscription_days?: number;
|
||||
promo_traffic_gb?: number;
|
||||
}
|
||||
|
||||
export interface AdminWheelConfig {
|
||||
id: number
|
||||
is_enabled: boolean
|
||||
name: string
|
||||
spin_cost_stars: number
|
||||
spin_cost_days: number
|
||||
spin_cost_stars_enabled: boolean
|
||||
spin_cost_days_enabled: boolean
|
||||
rtp_percent: number
|
||||
daily_spin_limit: number
|
||||
min_subscription_days_for_day_payment: number
|
||||
promo_prefix: string
|
||||
promo_validity_days: number
|
||||
prizes: WheelPrizeAdmin[]
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
id: number;
|
||||
is_enabled: boolean;
|
||||
name: string;
|
||||
spin_cost_stars: number;
|
||||
spin_cost_days: number;
|
||||
spin_cost_stars_enabled: boolean;
|
||||
spin_cost_days_enabled: boolean;
|
||||
rtp_percent: number;
|
||||
daily_spin_limit: number;
|
||||
min_subscription_days_for_day_payment: number;
|
||||
promo_prefix: string;
|
||||
promo_validity_days: number;
|
||||
prizes: WheelPrizeAdmin[];
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface WheelStatistics {
|
||||
total_spins: number
|
||||
total_revenue_kopeks: number
|
||||
total_payout_kopeks: number
|
||||
actual_rtp_percent: number
|
||||
configured_rtp_percent: number
|
||||
spins_by_payment_type: Record<string, { count: number; total_kopeks: number }>
|
||||
total_spins: number;
|
||||
total_revenue_kopeks: number;
|
||||
total_payout_kopeks: number;
|
||||
actual_rtp_percent: number;
|
||||
configured_rtp_percent: number;
|
||||
spins_by_payment_type: Record<string, { count: number; total_kopeks: number }>;
|
||||
prizes_distribution: Array<{
|
||||
prize_type: string
|
||||
display_name: string
|
||||
count: number
|
||||
total_kopeks: number
|
||||
}>
|
||||
prize_type: string;
|
||||
display_name: string;
|
||||
count: number;
|
||||
total_kopeks: number;
|
||||
}>;
|
||||
top_wins: Array<{
|
||||
user_id: number
|
||||
username: string | null
|
||||
prize_display_name: string
|
||||
prize_value_kopeks: number
|
||||
created_at: string | null
|
||||
}>
|
||||
period_from: string | null
|
||||
period_to: string | null
|
||||
user_id: number;
|
||||
username: string | null;
|
||||
prize_display_name: string;
|
||||
prize_value_kopeks: number;
|
||||
created_at: string | null;
|
||||
}>;
|
||||
period_from: string | null;
|
||||
period_to: string | null;
|
||||
}
|
||||
|
||||
export interface AdminSpinItem {
|
||||
id: number
|
||||
user_id: number
|
||||
username: string | null
|
||||
payment_type: string
|
||||
payment_amount: number
|
||||
payment_value_kopeks: number
|
||||
prize_type: string
|
||||
prize_value: number
|
||||
prize_display_name: string
|
||||
prize_value_kopeks: number
|
||||
is_applied: boolean
|
||||
created_at: string
|
||||
id: number;
|
||||
user_id: number;
|
||||
username: string | null;
|
||||
payment_type: string;
|
||||
payment_amount: number;
|
||||
payment_value_kopeks: number;
|
||||
prize_type: string;
|
||||
prize_value: number;
|
||||
prize_display_name: string;
|
||||
prize_value_kopeks: number;
|
||||
is_applied: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AdminSpinsResponse {
|
||||
items: AdminSpinItem[]
|
||||
total: number
|
||||
page: number
|
||||
per_page: number
|
||||
pages: number
|
||||
items: AdminSpinItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
// ==================== USER API ====================
|
||||
@@ -184,101 +184,107 @@ export interface AdminSpinsResponse {
|
||||
export const wheelApi = {
|
||||
// Get wheel config
|
||||
getConfig: async (): Promise<WheelConfig> => {
|
||||
const response = await apiClient.get<WheelConfig>('/cabinet/wheel/config')
|
||||
return response.data
|
||||
const response = await apiClient.get<WheelConfig>('/cabinet/wheel/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Check spin availability
|
||||
checkAvailability: async (): Promise<SpinAvailability> => {
|
||||
const response = await apiClient.get<SpinAvailability>('/cabinet/wheel/availability')
|
||||
return response.data
|
||||
const response = await apiClient.get<SpinAvailability>('/cabinet/wheel/availability');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Spin the wheel
|
||||
spin: async (paymentType: 'telegram_stars' | 'subscription_days'): Promise<SpinResult> => {
|
||||
const response = await apiClient.post<SpinResult>('/cabinet/wheel/spin', {
|
||||
payment_type: paymentType,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get spin history
|
||||
getHistory: async (page = 1, perPage = 20): Promise<SpinHistoryResponse> => {
|
||||
const response = await apiClient.get<SpinHistoryResponse>('/cabinet/wheel/history', {
|
||||
params: { page, per_page: perPage },
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create Stars invoice for Mini App payment
|
||||
createStarsInvoice: async (): Promise<StarsInvoiceResponse> => {
|
||||
const response = await apiClient.post<StarsInvoiceResponse>('/cabinet/wheel/stars-invoice')
|
||||
return response.data
|
||||
const response = await apiClient.post<StarsInvoiceResponse>('/cabinet/wheel/stars-invoice');
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== ADMIN API ====================
|
||||
|
||||
export const adminWheelApi = {
|
||||
// Get full config
|
||||
getConfig: async (): Promise<AdminWheelConfig> => {
|
||||
const response = await apiClient.get<AdminWheelConfig>('/cabinet/admin/wheel/config')
|
||||
return response.data
|
||||
const response = await apiClient.get<AdminWheelConfig>('/cabinet/admin/wheel/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update config
|
||||
updateConfig: async (data: Partial<AdminWheelConfig>): Promise<AdminWheelConfig> => {
|
||||
const response = await apiClient.put<AdminWheelConfig>('/cabinet/admin/wheel/config', data)
|
||||
return response.data
|
||||
const response = await apiClient.put<AdminWheelConfig>('/cabinet/admin/wheel/config', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get prizes
|
||||
getPrizes: async (): Promise<WheelPrizeAdmin[]> => {
|
||||
const response = await apiClient.get<WheelPrizeAdmin[]>('/cabinet/admin/wheel/prizes')
|
||||
return response.data
|
||||
const response = await apiClient.get<WheelPrizeAdmin[]>('/cabinet/admin/wheel/prizes');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create prize
|
||||
createPrize: async (data: CreateWheelPrizeData): Promise<WheelPrizeAdmin> => {
|
||||
const response = await apiClient.post<WheelPrizeAdmin>('/cabinet/admin/wheel/prizes', data)
|
||||
return response.data
|
||||
const response = await apiClient.post<WheelPrizeAdmin>('/cabinet/admin/wheel/prizes', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update prize
|
||||
updatePrize: async (prizeId: number, data: Partial<WheelPrizeAdmin>): Promise<WheelPrizeAdmin> => {
|
||||
const response = await apiClient.put<WheelPrizeAdmin>(`/cabinet/admin/wheel/prizes/${prizeId}`, data)
|
||||
return response.data
|
||||
updatePrize: async (
|
||||
prizeId: number,
|
||||
data: Partial<WheelPrizeAdmin>,
|
||||
): Promise<WheelPrizeAdmin> => {
|
||||
const response = await apiClient.put<WheelPrizeAdmin>(
|
||||
`/cabinet/admin/wheel/prizes/${prizeId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete prize
|
||||
deletePrize: async (prizeId: number): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/wheel/prizes/${prizeId}`)
|
||||
await apiClient.delete(`/cabinet/admin/wheel/prizes/${prizeId}`);
|
||||
},
|
||||
|
||||
// Reorder prizes
|
||||
reorderPrizes: async (prizeIds: number[]): Promise<void> => {
|
||||
await apiClient.post('/cabinet/admin/wheel/prizes/reorder', { prize_ids: prizeIds })
|
||||
await apiClient.post('/cabinet/admin/wheel/prizes/reorder', { prize_ids: prizeIds });
|
||||
},
|
||||
|
||||
// Get statistics
|
||||
getStatistics: async (dateFrom?: string, dateTo?: string): Promise<WheelStatistics> => {
|
||||
const response = await apiClient.get<WheelStatistics>('/cabinet/admin/wheel/statistics', {
|
||||
params: { date_from: dateFrom, date_to: dateTo },
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all spins
|
||||
getSpins: async (params?: {
|
||||
user_id?: number
|
||||
date_from?: string
|
||||
date_to?: string
|
||||
page?: number
|
||||
per_page?: number
|
||||
user_id?: number;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}): Promise<AdminSpinsResponse> => {
|
||||
const response = await apiClient.get<AdminSpinsResponse>('/cabinet/admin/wheel/spins', {
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user