mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Admin can now attach photos, videos, and documents when replying to tickets. Upload happens on file selection via existing media upload endpoint. Includes attachment preview with type-specific icons, upload progress, error handling, race condition protection, and proper blob URL lifecycle management. Added video player rendering for video attachments in message thread. Updated ticket list preview to show [media_type] for media-only messages.
381 lines
9.6 KiB
TypeScript
381 lines
9.6 KiB
TypeScript
import apiClient from './client';
|
|
|
|
export interface AdminTicketUser {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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[];
|
|
}
|
|
|
|
export interface AdminTicketStats {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface AdminTicketListResponse {
|
|
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;
|
|
},
|
|
|
|
// Get ticket statistics
|
|
getTicketStats: async (): Promise<AdminTicketStats> => {
|
|
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;
|
|
user_id?: number;
|
|
} = {},
|
|
): 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;
|
|
},
|
|
|
|
// Reply to ticket
|
|
replyToTicket: async (
|
|
ticketId: number,
|
|
message: string,
|
|
media?: { media_type?: string; media_file_id?: string; media_caption?: string },
|
|
): Promise<AdminTicketMessage> => {
|
|
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/reply`, {
|
|
message,
|
|
...media,
|
|
});
|
|
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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// Get ticket settings
|
|
getTicketSettings: async (): Promise<TicketSettings> => {
|
|
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;
|
|
},
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
export interface NodesOverview {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface ServerStats {
|
|
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;
|
|
}
|
|
|
|
export interface TariffStats {
|
|
tariffs: TariffStatItem[];
|
|
total_tariff_subscriptions: number;
|
|
}
|
|
|
|
export interface DashboardStats {
|
|
nodes: NodesOverview;
|
|
subscriptions: SubscriptionStats;
|
|
financial: FinancialStats;
|
|
servers: ServerStats;
|
|
revenue_chart: RevenueData[];
|
|
tariff_stats?: TariffStats;
|
|
}
|
|
|
|
export interface TopReferrerItem {
|
|
user_id: number;
|
|
telegram_id: number | null;
|
|
username?: string;
|
|
email?: string | null;
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export interface TopCampaignsResponse {
|
|
campaigns: TopCampaignItem[];
|
|
total_campaigns: number;
|
|
total_registrations: number;
|
|
total_revenue_kopeks: number;
|
|
}
|
|
|
|
export interface RecentPaymentItem {
|
|
id: number;
|
|
user_id: number;
|
|
telegram_id: number | null;
|
|
email?: string | null;
|
|
username?: string | null;
|
|
display_name: string;
|
|
amount_kopeks: number;
|
|
amount_rubles: number;
|
|
type: string;
|
|
type_display: string;
|
|
payment_method?: string | null;
|
|
description?: string | null;
|
|
created_at: string;
|
|
is_completed: boolean;
|
|
}
|
|
|
|
export interface RecentPaymentsResponse {
|
|
payments: RecentPaymentItem[];
|
|
total_count: number;
|
|
total_today_kopeks: number;
|
|
total_week_kopeks: number;
|
|
}
|
|
|
|
export interface SystemInfo {
|
|
bot_version: string;
|
|
python_version: string;
|
|
uptime_seconds: number;
|
|
users_total: number;
|
|
subscriptions_active: number;
|
|
}
|
|
|
|
export const statsApi = {
|
|
// Get system info
|
|
getSystemInfo: async (): Promise<SystemInfo> => {
|
|
const response = await apiClient.get('/cabinet/admin/stats/system-info');
|
|
return response.data;
|
|
},
|
|
|
|
// Get complete dashboard stats
|
|
getDashboardStats: async (): Promise<DashboardStats> => {
|
|
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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
};
|