feat: add partner management and withdrawal admin pages

- Admin partners page: list partners, view applications, approve/reject
- Admin partner detail: stats, commission management, campaign assignment
- Admin withdrawals page: list with risk scoring, status filters including cancelled
- Admin withdrawal detail: fraud analysis, approve/reject/complete actions
- Partner API client with full TypeScript interfaces
- Withdrawal API client with admin and user endpoints
- Referral page: partner application form, withdrawal balance and history
- Navigation: partners and withdrawals in admin panel marketing group
- i18n: full translations for ru, en, zh, fa locales
- Neutral fallbacks for unknown status badges
This commit is contained in:
Fringg
2026-02-17 09:51:48 +03:00
parent e0dd21fd0b
commit 779fbf0dc6
13 changed files with 3322 additions and 15 deletions

181
src/api/partners.ts Normal file
View File

@@ -0,0 +1,181 @@
import apiClient from './client';
// ==================== User-facing types ====================
export interface PartnerApplicationInfo {
id: number;
status: string;
company_name: string | null;
website_url: string | null;
telegram_channel: string | null;
description: string | null;
expected_monthly_referrals: number | null;
admin_comment: string | null;
approved_commission_percent: number | null;
created_at: string;
processed_at: string | null;
}
export interface PartnerStatusResponse {
partner_status: string;
commission_percent: number | null;
latest_application: PartnerApplicationInfo | null;
}
export interface PartnerApplicationRequest {
company_name?: string;
website_url?: string;
telegram_channel?: string;
description?: string;
expected_monthly_referrals?: number;
}
// ==================== Admin-facing types ====================
export interface AdminPartnerApplicationItem {
id: number;
user_id: number;
username: string | null;
first_name: string | null;
telegram_id: number | null;
company_name: string | null;
website_url: string | null;
telegram_channel: string | null;
description: string | null;
expected_monthly_referrals: number | null;
status: string;
admin_comment: string | null;
approved_commission_percent: number | null;
created_at: string;
processed_at: string | null;
}
export interface AdminPartnerApplicationsResponse {
items: AdminPartnerApplicationItem[];
total: number;
}
export interface AdminPartnerItem {
user_id: number;
username: string | null;
first_name: string | null;
telegram_id: number | null;
commission_percent: number | null;
total_referrals: number;
total_earnings_kopeks: number;
balance_kopeks: number;
partner_status: string;
created_at: string;
}
export interface AdminPartnerListResponse {
items: AdminPartnerItem[];
total: number;
}
export interface AdminPartnerDetailResponse {
user_id: number;
username: string | null;
first_name: string | null;
telegram_id: number | null;
commission_percent: number | null;
partner_status: string;
balance_kopeks: number;
total_referrals: number;
paid_referrals: number;
active_referrals: number;
earnings_all_time: number;
earnings_today: number;
earnings_week: number;
earnings_month: number;
conversion_to_paid: number;
campaigns: { id: number; name: string; start_parameter: string; is_active: boolean }[];
created_at: string;
}
export interface PartnerStats {
total_partners: number;
pending_applications: number;
total_referrals: number;
total_earnings_kopeks: number;
}
export const partnerApi = {
// User endpoints
getStatus: async (): Promise<PartnerStatusResponse> => {
const response = await apiClient.get<PartnerStatusResponse>('/cabinet/referral/partner/status');
return response.data;
},
apply: async (data: PartnerApplicationRequest): Promise<PartnerApplicationInfo> => {
const response = await apiClient.post<PartnerApplicationInfo>(
'/cabinet/referral/partner/apply',
data,
);
return response.data;
},
// Admin endpoints
getStats: async (): Promise<PartnerStats> => {
const response = await apiClient.get<PartnerStats>('/cabinet/admin/partners/stats');
return response.data;
},
getApplications: async (params?: {
status?: string;
offset?: number;
limit?: number;
}): Promise<AdminPartnerApplicationsResponse> => {
const response = await apiClient.get<AdminPartnerApplicationsResponse>(
'/cabinet/admin/partners/applications',
{ params },
);
return response.data;
},
approveApplication: async (
applicationId: number,
data: { commission_percent: number; comment?: string },
): Promise<void> => {
await apiClient.post(`/cabinet/admin/partners/applications/${applicationId}/approve`, data);
},
rejectApplication: async (applicationId: number, data: { comment?: string }): Promise<void> => {
await apiClient.post(`/cabinet/admin/partners/applications/${applicationId}/reject`, data);
},
getPartners: async (params?: {
offset?: number;
limit?: number;
}): Promise<AdminPartnerListResponse> => {
const response = await apiClient.get<AdminPartnerListResponse>('/cabinet/admin/partners', {
params,
});
return response.data;
},
getPartnerDetail: async (userId: number): Promise<AdminPartnerDetailResponse> => {
const response = await apiClient.get<AdminPartnerDetailResponse>(
`/cabinet/admin/partners/${userId}`,
);
return response.data;
},
updateCommission: async (userId: number, commissionPercent: number): Promise<void> => {
await apiClient.patch(`/cabinet/admin/partners/${userId}/commission`, {
commission_percent: commissionPercent,
});
},
revokePartner: async (userId: number): Promise<void> => {
await apiClient.post(`/cabinet/admin/partners/${userId}/revoke`);
},
assignCampaign: async (userId: number, campaignId: number): Promise<void> => {
await apiClient.post(`/cabinet/admin/partners/${userId}/campaigns/${campaignId}/assign`);
},
unassignCampaign: async (userId: number, campaignId: number): Promise<void> => {
await apiClient.post(`/cabinet/admin/partners/${userId}/campaigns/${campaignId}/unassign`);
},
};

152
src/api/withdrawals.ts Normal file
View File

@@ -0,0 +1,152 @@
import apiClient from './client';
// ==================== User-facing types ====================
export interface WithdrawalBalanceResponse {
total_earned: number;
referral_spent: number;
withdrawn: number;
pending: number;
available_referral: number;
available_total: number;
only_referral_mode: boolean;
min_amount_kopeks: number;
is_withdrawal_enabled: boolean;
can_request: boolean;
cannot_request_reason: string | null;
}
export interface WithdrawalCreateRequest {
amount_kopeks: number;
payment_details: string;
}
export interface WithdrawalItem {
id: number;
amount_kopeks: number;
amount_rubles: number;
status: string;
payment_details: string | null;
admin_comment: string | null;
created_at: string;
processed_at: string | null;
}
export interface WithdrawalListResponse {
items: WithdrawalItem[];
total: number;
}
export interface WithdrawalCreateResponse {
id: number;
amount_kopeks: number;
status: string;
}
// ==================== Admin-facing types ====================
export interface AdminWithdrawalItem {
id: number;
user_id: number;
username: string | null;
first_name: string | null;
telegram_id: number | null;
amount_kopeks: number;
amount_rubles: number;
status: string;
risk_score: number;
risk_level: string;
payment_details: string | null;
admin_comment: string | null;
created_at: string;
processed_at: string | null;
}
export interface AdminWithdrawalListResponse {
items: AdminWithdrawalItem[];
total: number;
pending_count: number;
pending_total_kopeks: number;
}
export interface AdminWithdrawalDetailResponse {
id: number;
user_id: number;
username: string | null;
first_name: string | null;
telegram_id: number | null;
amount_kopeks: number;
amount_rubles: number;
status: string;
risk_score: number;
risk_level: string;
risk_analysis: Record<string, unknown> | null;
payment_details: string | null;
admin_comment: string | null;
balance_kopeks: number;
total_referrals: number;
total_earnings_kopeks: number;
created_at: string;
processed_at: string | null;
}
export const withdrawalApi = {
// User endpoints
getBalance: async (): Promise<WithdrawalBalanceResponse> => {
const response = await apiClient.get<WithdrawalBalanceResponse>(
'/cabinet/referral/withdrawal/balance',
);
return response.data;
},
create: async (data: WithdrawalCreateRequest): Promise<WithdrawalCreateResponse> => {
const response = await apiClient.post<WithdrawalCreateResponse>(
'/cabinet/referral/withdrawal/create',
data,
);
return response.data;
},
getHistory: async (): Promise<WithdrawalListResponse> => {
const response = await apiClient.get<WithdrawalListResponse>(
'/cabinet/referral/withdrawal/history',
);
return response.data;
},
cancel: async (requestId: number): Promise<void> => {
await apiClient.post(`/cabinet/referral/withdrawal/${requestId}/cancel`);
},
// Admin endpoints
getAll: async (params?: {
status?: string;
offset?: number;
limit?: number;
}): Promise<AdminWithdrawalListResponse> => {
const response = await apiClient.get<AdminWithdrawalListResponse>(
'/cabinet/admin/withdrawals',
{ params },
);
return response.data;
},
getDetail: async (withdrawalId: number): Promise<AdminWithdrawalDetailResponse> => {
const response = await apiClient.get<AdminWithdrawalDetailResponse>(
`/cabinet/admin/withdrawals/${withdrawalId}`,
);
return response.data;
},
approve: async (withdrawalId: number, comment?: string): Promise<void> => {
await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/approve`, { comment });
},
reject: async (withdrawalId: number, comment?: string): Promise<void> => {
await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/reject`, { comment });
},
complete: async (withdrawalId: number): Promise<void> => {
await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/complete`);
},
};