diff --git a/src/App.tsx b/src/App.tsx index 31eeb9f..b39bd3e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -59,6 +59,18 @@ const AdminCampaigns = lazy(() => import('./pages/AdminCampaigns')); const AdminCampaignCreate = lazy(() => import('./pages/AdminCampaignCreate')); const AdminCampaignStats = lazy(() => import('./pages/AdminCampaignStats')); const AdminCampaignEdit = lazy(() => import('./pages/AdminCampaignEdit')); +const AdminPartners = lazy(() => import('./pages/AdminPartners')); +const AdminPartnerSettings = lazy(() => import('./pages/AdminPartnerSettings')); +const AdminPartnerDetail = lazy(() => import('./pages/AdminPartnerDetail')); +const AdminApplicationReview = lazy(() => import('./pages/AdminApplicationReview')); +const AdminPartnerCommission = lazy(() => import('./pages/AdminPartnerCommission')); +const AdminPartnerRevoke = lazy(() => import('./pages/AdminPartnerRevoke')); +const AdminPartnerCampaignAssign = lazy(() => import('./pages/AdminPartnerCampaignAssign')); +const AdminWithdrawals = lazy(() => import('./pages/AdminWithdrawals')); +const AdminWithdrawalDetail = lazy(() => import('./pages/AdminWithdrawalDetail')); +const AdminWithdrawalReject = lazy(() => import('./pages/AdminWithdrawalReject')); +const ReferralPartnerApply = lazy(() => import('./pages/ReferralPartnerApply')); +const ReferralWithdrawalRequest = lazy(() => import('./pages/ReferralWithdrawalRequest')); const AdminUsers = lazy(() => import('./pages/AdminUsers')); const AdminPayments = lazy(() => import('./pages/AdminPayments')); const AdminPaymentMethods = lazy(() => import('./pages/AdminPaymentMethods')); @@ -217,6 +229,26 @@ function App() { } /> + + + + + + } + /> + + + + + + } + /> } /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> => { @@ -240,4 +252,10 @@ export const campaignsApi = { const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs'); return response.data; }, + + // Get available partners for campaign assignment + getAvailablePartners: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/campaigns/available-partners'); + return response.data; + }, }; diff --git a/src/api/partners.ts b/src/api/partners.ts new file mode 100644 index 0000000..007474f --- /dev/null +++ b/src/api/partners.ts @@ -0,0 +1,228 @@ +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 PartnerCampaignInfo { + id: number; + name: string; + start_parameter: string; + bonus_type: string; + balance_bonus_kopeks: number; + subscription_duration_days: number | null; + subscription_traffic_gb: number | null; + deep_link: string | null; + web_link: string | null; +} + +export interface PartnerStatusResponse { + partner_status: string; + commission_percent: number | null; + latest_application: PartnerApplicationInfo | null; + campaigns: PartnerCampaignInfo[]; +} + +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; +} + +// ==================== Partner Settings types ==================== + +export interface PartnerSettings { + withdrawal_enabled: boolean; + withdrawal_min_amount_kopeks: number; + withdrawal_cooldown_days: number; + withdrawal_requisites_text: string; + partner_section_visible: boolean; + referral_program_enabled: boolean; +} + +export interface PartnerSettingsUpdate { + withdrawal_enabled?: boolean; + withdrawal_min_amount_kopeks?: number; + withdrawal_cooldown_days?: number; + withdrawal_requisites_text?: string; + partner_section_visible?: boolean; + referral_program_enabled?: boolean; +} + +export const partnerApi = { + // User endpoints + getStatus: async (): Promise => { + const response = await apiClient.get('/cabinet/referral/partner/status'); + return response.data; + }, + + apply: async (data: PartnerApplicationRequest): Promise => { + const response = await apiClient.post( + '/cabinet/referral/partner/apply', + data, + ); + return response.data; + }, + + // Admin endpoints + getStats: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/partners/stats'); + return response.data; + }, + + getApplications: async (params?: { + status?: string; + offset?: number; + limit?: number; + }): Promise => { + const response = await apiClient.get( + '/cabinet/admin/partners/applications', + { params }, + ); + return response.data; + }, + + approveApplication: async ( + applicationId: number, + data: { commission_percent: number; comment?: string }, + ): Promise => { + await apiClient.post(`/cabinet/admin/partners/applications/${applicationId}/approve`, data); + }, + + rejectApplication: async (applicationId: number, data: { comment?: string }): Promise => { + await apiClient.post(`/cabinet/admin/partners/applications/${applicationId}/reject`, data); + }, + + getPartners: async (params?: { + offset?: number; + limit?: number; + }): Promise => { + const response = await apiClient.get('/cabinet/admin/partners', { + params, + }); + return response.data; + }, + + getPartnerDetail: async (userId: number): Promise => { + const response = await apiClient.get( + `/cabinet/admin/partners/${userId}`, + ); + return response.data; + }, + + updateCommission: async (userId: number, commissionPercent: number): Promise => { + await apiClient.patch(`/cabinet/admin/partners/${userId}/commission`, { + commission_percent: commissionPercent, + }); + }, + + revokePartner: async (userId: number): Promise => { + await apiClient.post(`/cabinet/admin/partners/${userId}/revoke`); + }, + + assignCampaign: async (userId: number, campaignId: number): Promise => { + await apiClient.post(`/cabinet/admin/partners/${userId}/campaigns/${campaignId}/assign`); + }, + + unassignCampaign: async (userId: number, campaignId: number): Promise => { + await apiClient.post(`/cabinet/admin/partners/${userId}/campaigns/${campaignId}/unassign`); + }, + + // Settings + getPartnerSettings: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/partners/settings'); + return response.data; + }, + + updatePartnerSettings: async (data: PartnerSettingsUpdate): Promise => { + const response = await apiClient.patch( + '/cabinet/admin/partners/settings', + data, + ); + return response.data; + }, +}; diff --git a/src/api/withdrawals.ts b/src/api/withdrawals.ts new file mode 100644 index 0000000..f7c385b --- /dev/null +++ b/src/api/withdrawals.ts @@ -0,0 +1,153 @@ +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; + requisites_text: string; +} + +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 | 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 => { + const response = await apiClient.get( + '/cabinet/referral/withdrawal/balance', + ); + return response.data; + }, + + create: async (data: WithdrawalCreateRequest): Promise => { + const response = await apiClient.post( + '/cabinet/referral/withdrawal/create', + data, + ); + return response.data; + }, + + getHistory: async (): Promise => { + const response = await apiClient.get( + '/cabinet/referral/withdrawal/history', + ); + return response.data; + }, + + cancel: async (requestId: number): Promise => { + await apiClient.post(`/cabinet/referral/withdrawal/${requestId}/cancel`); + }, + + // Admin endpoints + getAll: async (params?: { + status?: string; + offset?: number; + limit?: number; + }): Promise => { + const response = await apiClient.get( + '/cabinet/admin/withdrawals', + { params }, + ); + return response.data; + }, + + getDetail: async (withdrawalId: number): Promise => { + const response = await apiClient.get( + `/cabinet/admin/withdrawals/${withdrawalId}`, + ); + return response.data; + }, + + approve: async (withdrawalId: number, comment?: string): Promise => { + await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/approve`, { comment }); + }, + + reject: async (withdrawalId: number, comment?: string): Promise => { + await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/reject`, { comment }); + }, + + complete: async (withdrawalId: number): Promise => { + await apiClient.post(`/cabinet/admin/withdrawals/${withdrawalId}/complete`); + }, +}; diff --git a/src/components/connection/blocks/AccordionBlock.tsx b/src/components/connection/blocks/AccordionBlock.tsx index f0ee2d0..fd46c36 100644 --- a/src/components/connection/blocks/AccordionBlock.tsx +++ b/src/components/connection/blocks/AccordionBlock.tsx @@ -13,9 +13,15 @@ export function AccordionBlock({ }: BlockRendererProps) { const [openIndex, setOpenIndex] = useState(0); + const visibleBlocks = blocks.filter( + (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length, + ); + + if (!visibleBlocks.length) return null; + return (
- {blocks.map((block, index) => { + {visibleBlocks.map((block, index) => { const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight); const isOpen = openIndex === index; diff --git a/src/components/connection/blocks/CardsBlock.tsx b/src/components/connection/blocks/CardsBlock.tsx index 0c528d8..5ef48c5 100644 --- a/src/components/connection/blocks/CardsBlock.tsx +++ b/src/components/connection/blocks/CardsBlock.tsx @@ -10,9 +10,15 @@ export function CardsBlock({ getSvgHtml, renderBlockButtons, }: BlockRendererProps) { + const visibleBlocks = blocks.filter( + (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length, + ); + + if (!visibleBlocks.length) return null; + return (
- {blocks.map((block, index) => { + {visibleBlocks.map((block, index) => { const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight); return ( diff --git a/src/components/connection/blocks/MinimalBlock.tsx b/src/components/connection/blocks/MinimalBlock.tsx index ff2e5f5..e5abb99 100644 --- a/src/components/connection/blocks/MinimalBlock.tsx +++ b/src/components/connection/blocks/MinimalBlock.tsx @@ -10,11 +10,17 @@ export function MinimalBlock({ getSvgHtml, renderBlockButtons, }: BlockRendererProps) { + const visibleBlocks = blocks.filter( + (b) => getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length, + ); + + if (!visibleBlocks.length) return null; + return (
- {blocks.map((block, index) => { + {visibleBlocks.map((block, index) => { const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight); - const isLast = index === blocks.length - 1; + const isLast = index === visibleBlocks.length - 1; return (
getLocalizedText(b.title) || getLocalizedText(b.description) || b.buttons?.length, + ); + + if (!visibleBlocks.length) return null; + return (
- {blocks.map((block, index) => { + {visibleBlocks.map((block, index) => { const gradientStyle = getColorGradientSolid(block.svgIconColor || 'cyan', isLight); - const isLast = index === blocks.length - 1; + const isLast = index === visibleBlocks.length - 1; return (
diff --git a/src/locales/en.json b/src/locales/en.json index a0cd87a..ea37eb5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -243,6 +243,14 @@ "serversLabel": "Servers", "servers_one": "{{count}} server", "servers_other": "{{count}} servers", + "locationsLabel": "Locations", + "locations_one": "{{count}} location", + "locations_other": "{{count}} locations", + "trafficReset": { + "DAY": "Resets daily", + "WEEK": "Resets weekly", + "MONTH": "Resets monthly" + }, "traffic": "Traffic", "unlimited": "Unlimited", "used": "Used", @@ -652,7 +660,83 @@ "referral_subscription_renewal": "Referral Subscription Renewal", "referral_bonus": "Referral Bonus" }, - "disabled": "Referral program is currently disabled" + "disabled": "Referral program is currently disabled", + "partner": { + "becomePartner": "Become a Partner", + "becomePartnerDesc": "Apply for the partner program to get higher commission rates and the ability to withdraw your earnings.", + "applyButton": "Apply", + "applyTitle": "Partner Application", + "applyDesc": "Fill out the form below to apply for our partner program. All fields are optional.", + "underReview": "Application Under Review", + "underReviewDesc": "Your partner application is being reviewed. We'll notify you once a decision is made.", + "submittedAt": "Submitted on {{date}}", + "partnerStatus": "Partner", + "active": "Active", + "commissionInfo": "Your partner commission rate is {{percent}}%", + "rejected": "Application Rejected", + "reapplyButton": "Reapply", + "applying": "Submitting...", + "submitApplication": "Submit Application", + "applyError": "Failed to submit application. Please try again.", + "yourCampaigns": "Your campaigns", + "campaignLinks": { + "bot": "Bot link", + "web": "Cabinet link" + }, + "campaignBonus": { + "title": "Referral bonus", + "balance": "+{{amount}} to balance", + "balanceDesc": "{{amount}} credited on signup", + "subscription": "{{days}} days subscription", + "subscriptionDesc": "{{days}} days free subscription", + "tariff": "Bonus tariff", + "tariffDesc": "Free tariff plan" + }, + "fields": { + "companyName": "Company Name", + "companyNamePlaceholder": "Your company or brand name", + "telegramChannel": "Telegram Channel", + "telegramChannelPlaceholder": "@yourchannel", + "websiteUrl": "Website URL", + "websiteUrlPlaceholder": "https://example.com", + "description": "Description", + "descriptionPlaceholder": "Tell us about your audience and how you plan to promote...", + "expectedReferrals": "Expected Monthly Referrals", + "expectedReferralsPlaceholder": "Estimated number per month" + } + }, + "withdrawal": { + "title": "Withdrawal", + "goToWithdrawal": "Withdraw", + "available": "Available", + "totalEarned": "Total Earned", + "withdrawn": "Withdrawn", + "spent": "Spent on Services", + "pending": "Pending", + "requestButton": "Request Withdrawal", + "minAmount": "Minimum withdrawal amount: {{amount}}", + "history": "Withdrawal History", + "noHistory": "No withdrawal requests yet", + "requestTitle": "Request Withdrawal", + "requestDesc": "Available for withdrawal: {{available}}", + "requesting": "Submitting...", + "submitRequest": "Submit Request", + "requestError": "Failed to create withdrawal request. Please try again.", + "fields": { + "amount": "Amount (kopeks)", + "amountPlaceholder": "Enter amount in kopeks", + "amountHint": "Enter the amount in kopeks (100 kopeks = 1 {{currency}})", + "paymentDetails": "Payment Details", + "paymentDetailsPlaceholder": "Card number, wallet address, or other payment info..." + }, + "status": { + "pending": "Pending", + "approved": "Approved", + "completed": "Completed", + "rejected": "Rejected", + "cancelled": "Cancelled" + } + } }, "support": { "title": "Support", @@ -775,7 +859,9 @@ "promoGroups": "Discount Groups", "trafficUsage": "Traffic Usage", "updates": "Updates", - "pinnedMessages": "Pinned Messages" + "pinnedMessages": "Pinned Messages", + "partners": "Partners", + "withdrawals": "Withdrawals" }, "panel": { "title": "Admin Panel", @@ -800,7 +886,9 @@ "promoGroupsDesc": "Discount groups for users", "trafficUsageDesc": "Per-user traffic by nodes", "updatesDesc": "Bot & cabinet versions", - "pinnedMessagesDesc": "Manage pinned messages for users" + "pinnedMessagesDesc": "Manage pinned messages for users", + "partnersDesc": "Manage partner applications and commissions", + "withdrawalsDesc": "Review and process withdrawal requests" }, "trafficUsage": { "title": "Traffic Usage", @@ -1116,6 +1204,8 @@ "total": "Total", "sent": "Sent", "failed": "Failed", + "blocked": "Blocked", + "blockedShort": "blocked", "filter": "Filter", "message": "Message", "unknownAdmin": "Unknown", @@ -1665,7 +1755,11 @@ "active": "Active", "cancel": "Cancel", "save": "Save", - "saving": "Saving..." + "saving": "Saving...", + "partner": "Partner", + "noPartner": "No partner", + "selectPartner": "Select partner", + "partnerAutoAssign": "Campaign will be automatically assigned to partner {{name}}" }, "stats": { "title": "Campaign Statistics", @@ -1721,6 +1815,189 @@ "loadError": "Failed to load campaign", "updateError": "Failed to save changes" }, + "withdrawals": { + "title": "Withdrawals", + "subtitle": "Manage withdrawal requests", + "noData": "No withdrawal requests", + "risk": "Risk:", + "overview": { + "pendingCount": "Pending", + "pendingAmount": "Pending amount", + "approvedCount": "Approved", + "completedCount": "Completed" + }, + "filter": { + "all": "All", + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "completed": "Completed", + "cancelled": "Cancelled" + }, + "status": { + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "completed": "Completed", + "cancelled": "Cancelled", + "unknown": "Unknown" + }, + "detail": { + "title": "Withdrawal", + "loadError": "Failed to load withdrawal details", + "userInfo": "User Information", + "username": "Username", + "telegramId": "Telegram ID", + "balance": "Balance", + "totalReferrals": "Total referrals", + "totalEarnings": "Total earnings", + "createdAt": "Created", + "paymentDetails": "Payment Details", + "noPaymentDetails": "No payment details provided", + "riskAnalysis": "Risk Analysis", + "riskScore": "Risk score", + "riskLevel": { + "low": "Low", + "medium": "Medium", + "high": "High", + "critical": "Critical" + }, + "flags": "Flags", + "balanceStats": "Balance Statistics", + "referralDeposits": "Referral Deposits", + "suspiciousReferrals": "Suspicious Referrals", + "earningsByReason": "Earnings by Reason", + "adminComment": "Admin Comment", + "processedAt": "Processed at", + "approve": "Approve", + "approving": "Approving...", + "reject": "Reject", + "rejecting": "Rejecting...", + "complete": "Mark as Completed", + "completing": "Completing...", + "rejectTitle": "Reject withdrawal?", + "rejectDescription": "Optionally add a comment explaining the reason for rejection.", + "commentPlaceholder": "Reason for rejection (optional)...", + "confirmReject": "Confirm Rejection" + } + }, + "partners": { + "title": "Partners", + "subtitle": "Partner management and applications", + "totalPartners": "Total partners", + "pendingApplications": "Pending applications", + "totalReferrals": "Total referrals", + "totalEarnings": "Total earnings", + "tabs": { + "partners": "Partners", + "applications": "Applications" + }, + "noPartners": "No partners yet", + "noApplications": "No pending applications", + "commission": "{{percent}}% commission", + "referrals": "{{count}} referrals", + "actions": { + "approve": "Approve", + "reject": "Reject", + "review": "Review" + }, + "applicationFields": { + "companyName": "Company", + "website": "Website", + "channel": "Channel", + "description": "Description", + "expectedReferrals": "Expected referrals/mo" + }, + "approveDialog": { + "title": "Approve Application", + "description": "Approve partner application from {{name}}", + "commissionLabel": "Commission (%)" + }, + "rejectDialog": { + "title": "Reject Application", + "description": "Reject partner application from {{name}}", + "commentLabel": "Comment (optional)", + "commentPlaceholder": "Reason for rejection..." + }, + "settings": "Settings", + "settingsSubtitle": "Partner program and withdrawal settings", + "settingsLoadError": "Failed to load settings", + "settingsUpdateError": "Failed to save settings", + "settingsSection": { + "referralProgram": "Referral Program", + "withdrawalSettings": "Withdrawal Settings" + }, + "settingsFields": { + "programEnabled": "Referral program enabled", + "programEnabledDesc": "Enable/disable referral program for all users", + "partnerVisible": "Partner section visible in cabinet", + "partnerVisibleDesc": "Show/hide partner application and withdrawal sections for users", + "withdrawalEnabled": "Withdrawal enabled", + "withdrawalEnabledDesc": "Allow partners to request withdrawal of referral balance", + "minAmount": "Min. withdrawal amount (kopeks)", + "minAmountDesc": "Minimum amount for a withdrawal request (100 kopeks = 1 ₽)", + "cooldownDays": "Cooldown between withdrawals (days)", + "cooldownDaysDesc": "How many days must pass between withdrawal requests", + "requisitesText": "Requisites field prompt", + "requisitesTextDesc": "Custom text shown to users to describe what payment details to provide", + "requisitesTextPlaceholder": "E.g.: Provide your bank card number or USDT TRC-20 address" + } + }, + "partnerDetail": { + "title": "Partner Details", + "loadError": "Failed to load partner details", + "status": { + "approved": "Active", + "pending": "Pending", + "rejected": "Rejected", + "none": "Not a partner" + }, + "stats": { + "totalReferrals": "Total referrals", + "paidReferrals": "Paid referrals", + "activeReferrals": "Active referrals", + "conversionRate": "Conversion" + }, + "earnings": { + "title": "Earnings", + "allTime": "All time", + "today": "Today", + "week": "This week", + "month": "This month" + }, + "commission": { + "title": "Commission", + "update": "Update" + }, + "campaigns": { + "title": "Assigned Campaigns", + "noCampaigns": "No campaigns assigned", + "active": "Active", + "inactive": "Inactive", + "assign": "Assign", + "unassign": "Unassign", + "createNew": "Create", + "assignTitle": "Assign Campaign", + "noAvailable": "No available campaigns to assign", + "createTitle": "Create Campaign", + "createSubtitle": "Campaign will be auto-assigned to the partner", + "autoAssignNote": "Campaign will be automatically assigned to partner {{name}}", + "createAndAssign": "Create & Assign" + }, + "dangerZone": { + "title": "Danger Zone", + "revokeButton": "Revoke Partner Status" + }, + "commissionDialog": { + "title": "Update Commission", + "description": "Set a new commission percentage for this partner.", + "label": "Commission (%)" + }, + "revokeDialog": { + "title": "Revoke Partner Status?", + "description": "This will remove partner privileges. The partner will no longer earn commissions from referrals." + } + }, "promocodes": { "title": "Promo Codes", "subtitle": "Manage promo codes and discount groups", diff --git a/src/locales/fa.json b/src/locales/fa.json index b2a0e69..20cc322 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -239,6 +239,13 @@ "baseTariff": "تعرفه", "serversLabel": "سرورها", "servers_other": "{{count}} سرور", + "locationsLabel": "مکان‌ها", + "locations_other": "{{count}} مکان", + "trafficReset": { + "DAY": "بازنشانی روزانه", + "WEEK": "بازنشانی هفتگی", + "MONTH": "بازنشانی ماهانه" + }, "traffic": "ترافیک", "unlimited": "نامحدود", "used": "استفاده شده", @@ -543,7 +550,83 @@ "referral_subscription_renewal": "تمدید اشتراک کاربر معرفی‌شده", "referral_bonus": "پاداش کاربر معرفی‌شده" }, - "disabled": "برنامه معرفی موقتاً غیرفعال است" + "disabled": "برنامه معرفی موقتاً غیرفعال است", + "partner": { + "becomePartner": "شریک شوید", + "becomePartnerDesc": "برای دریافت نرخ کمیسیون بالاتر و امکان برداشت درآمد، درخواست شراکت دهید.", + "applyButton": "درخواست", + "applyTitle": "درخواست شراکت", + "applyDesc": "فرم زیر را برای درخواست برنامه شراکت پر کنید. تمام فیلدها اختیاری هستند.", + "underReview": "درخواست در حال بررسی", + "underReviewDesc": "درخواست شراکت شما در حال بررسی است. پس از تصمیم‌گیری به شما اطلاع خواهیم داد.", + "submittedAt": "ارسال شده در {{date}}", + "partnerStatus": "شریک", + "active": "فعال", + "commissionInfo": "نرخ کمیسیون شراکت شما {{percent}}% است", + "rejected": "درخواست رد شد", + "reapplyButton": "درخواست مجدد", + "applying": "در حال ارسال...", + "submitApplication": "ارسال درخواست", + "applyError": "ارسال درخواست ناموفق بود. لطفاً دوباره تلاش کنید.", + "yourCampaigns": "کمپین‌های شما", + "campaignLinks": { + "bot": "لینک ربات", + "web": "لینک کابینت" + }, + "campaignBonus": { + "title": "جایزه معرفی", + "balance": "+{{amount}} به موجودی", + "balanceDesc": "{{amount}} هنگام ثبت‌نام", + "subscription": "{{days}} روز اشتراک", + "subscriptionDesc": "{{days}} روز اشتراک رایگان", + "tariff": "تعرفه جایزه", + "tariffDesc": "طرح تعرفه رایگان" + }, + "fields": { + "companyName": "نام شرکت", + "companyNamePlaceholder": "نام شرکت یا برند شما", + "telegramChannel": "کانال تلگرام", + "telegramChannelPlaceholder": "@yourchannel", + "websiteUrl": "آدرس وب‌سایت", + "websiteUrlPlaceholder": "https://example.com", + "description": "توضیحات", + "descriptionPlaceholder": "درباره مخاطبان خود و نحوه تبلیغ بگویید...", + "expectedReferrals": "تعداد معرفی ماهانه مورد انتظار", + "expectedReferralsPlaceholder": "تعداد تخمینی در ماه" + } + }, + "withdrawal": { + "title": "برداشت", + "goToWithdrawal": "برداشت", + "available": "قابل برداشت", + "totalEarned": "مجموع درآمد", + "withdrawn": "برداشت شده", + "spent": "صرف خدمات شده", + "pending": "در انتظار", + "requestButton": "درخواست برداشت", + "minAmount": "حداقل مبلغ برداشت: {{amount}}", + "history": "تاریخچه برداشت", + "noHistory": "هنوز درخواست برداشتی وجود ندارد", + "requestTitle": "درخواست برداشت", + "requestDesc": "قابل برداشت: {{available}}", + "requesting": "در حال ارسال...", + "submitRequest": "ارسال درخواست", + "requestError": "ایجاد درخواست برداشت ناموفق بود. لطفاً دوباره تلاش کنید.", + "fields": { + "amount": "مبلغ (کوپک)", + "amountPlaceholder": "مبلغ را به کوپک وارد کنید", + "amountHint": "مبلغ را به کوپک وارد کنید (۱۰۰ کوپک = ۱ {{currency}})", + "paymentDetails": "اطلاعات پرداخت", + "paymentDetailsPlaceholder": "شماره کارت، آدرس کیف پول یا سایر اطلاعات پرداخت..." + }, + "status": { + "pending": "در انتظار", + "approved": "تأیید شده", + "completed": "تکمیل شده", + "rejected": "رد شده", + "cancelled": "لغو شده" + } + } }, "support": { "title": "پشتیبانی", @@ -656,7 +739,9 @@ "users": "کاربران", "trafficUsage": "مصرف ترافیک", "updates": "به‌روزرسانی‌ها", - "pinnedMessages": "پیام‌های سنجاق‌شده" + "pinnedMessages": "پیام‌های سنجاق‌شده", + "partners": "شرکا", + "withdrawals": "برداشت‌ها" }, "panel": { "title": "پنل مدیریت", @@ -681,7 +766,9 @@ "usersDesc": "مدیریت کاربران ربات", "trafficUsageDesc": "ترافیک هر کاربر بر اساس نود", "updatesDesc": "نسخه‌های ربات و کابینت", - "pinnedMessagesDesc": "مدیریت پیام‌های سنجاق‌شده" + "pinnedMessagesDesc": "مدیریت پیام‌های سنجاق‌شده", + "partnersDesc": "مدیریت درخواست‌های شراکت و کمیسیون‌ها", + "withdrawalsDesc": "بررسی و پردازش درخواست‌های برداشت" }, "trafficUsage": { "title": "مصرف ترافیک", @@ -837,6 +924,8 @@ "total": "کل", "sent": "ارسال شده", "failed": "ناموفق", + "blocked": "مسدود شده", + "blockedShort": "مسدود", "filter": "فیلتر", "message": "پیام", "unknownAdmin": "نامعلوم", @@ -1404,7 +1493,11 @@ "active": "فعال", "cancel": "لغو", "save": "ذخیره", - "saving": "در حال ذخیره..." + "saving": "در حال ذخیره...", + "partner": "شریک", + "noPartner": "بدون شریک", + "selectPartner": "انتخاب شریک", + "partnerAutoAssign": "کمپین به طور خودکار به شریک {{name}} اختصاص داده می‌شود" }, "stats": { "title": "آمار کمپین", @@ -1458,6 +1551,165 @@ "loadError": "بارگذاری کمپین ناموفق بود", "updateError": "ذخیره تغییرات ناموفق بود" }, + "withdrawals": { + "title": "برداشت‌ها", + "subtitle": "مدیریت درخواست‌های برداشت", + "noData": "درخواست برداشتی وجود ندارد", + "risk": "ریسک:", + "overview": { + "pendingCount": "در انتظار", + "pendingAmount": "مبلغ در انتظار", + "approvedCount": "تأیید شده", + "completedCount": "تکمیل شده" + }, + "filter": { + "all": "همه", + "pending": "در انتظار", + "approved": "تأیید شده", + "rejected": "رد شده", + "completed": "تکمیل شده", + "cancelled": "لغو شده" + }, + "status": { + "pending": "در انتظار", + "approved": "تأیید شده", + "rejected": "رد شده", + "completed": "تکمیل شده", + "cancelled": "لغو شده" + }, + "detail": { + "title": "جزئیات برداشت", + "loadError": "بارگذاری جزئیات برداشت ناموفق بود", + "userInfo": "اطلاعات کاربر", + "username": "نام کاربری", + "telegramId": "شناسه تلگرام", + "balance": "موجودی", + "totalReferrals": "مجموع معرفی‌ها", + "totalEarnings": "مجموع درآمد", + "createdAt": "تاریخ ایجاد", + "paymentDetails": "اطلاعات پرداخت", + "noPaymentDetails": "اطلاعات پرداختی ارائه نشده", + "riskAnalysis": "تحلیل ریسک", + "riskScore": "امتیاز ریسک", + "riskLevel": { + "low": "کم", + "medium": "متوسط", + "high": "زیاد", + "critical": "بحرانی" + }, + "flags": "پرچم‌ها", + "balanceStats": "آمار موجودی", + "referralDeposits": "واریزهای معرفی", + "suspiciousReferrals": "معرفی‌های مشکوک", + "earningsByReason": "درآمد بر اساس دلیل", + "adminComment": "نظر مدیر", + "processedAt": "زمان پردازش", + "approve": "تأیید", + "approving": "در حال تأیید...", + "reject": "رد", + "rejecting": "در حال رد...", + "complete": "تکمیل شده علامت بزنید", + "completing": "در حال تکمیل...", + "rejectTitle": "رد درخواست برداشت؟", + "rejectDescription": "در صورت تمایل دلیل رد را بنویسید.", + "commentPlaceholder": "دلیل رد (اختیاری)...", + "confirmReject": "تأیید رد" + } + }, + "partners": { + "title": "شرکا", + "subtitle": "مدیریت شرکا و درخواست‌ها", + "totalPartners": "مجموع شرکا", + "pendingApplications": "درخواست‌های در انتظار", + "totalReferrals": "مجموع معرفی‌ها", + "totalEarnings": "مجموع درآمد", + "tabs": { + "partners": "شرکا", + "applications": "درخواست‌ها" + }, + "noPartners": "هنوز شریکی وجود ندارد", + "noApplications": "درخواست در انتظاری وجود ندارد", + "commission": "{{percent}}% کمیسیون", + "referrals": "{{count}} معرفی", + "actions": { + "approve": "تأیید", + "reject": "رد", + "review": "بررسی" + }, + "applicationFields": { + "companyName": "شرکت", + "website": "وب‌سایت", + "channel": "کانال", + "description": "توضیحات", + "expectedReferrals": "معرفی مورد انتظار/ماه" + }, + "approveDialog": { + "title": "تأیید درخواست", + "description": "تأیید درخواست شراکت از {{name}}", + "commissionLabel": "کمیسیون (%)" + }, + "rejectDialog": { + "title": "رد درخواست", + "description": "رد درخواست شراکت از {{name}}", + "commentLabel": "نظر (اختیاری)", + "commentPlaceholder": "دلیل رد..." + } + }, + "partnerDetail": { + "title": "جزئیات شریک", + "loadError": "بارگذاری جزئیات شریک ناموفق بود", + "status": { + "approved": "فعال", + "pending": "در انتظار بررسی", + "rejected": "رد شده", + "none": "شریک نیست" + }, + "stats": { + "totalReferrals": "مجموع معرفی‌ها", + "paidReferrals": "معرفی‌های پرداختی", + "activeReferrals": "معرفی‌های فعال", + "conversionRate": "نرخ تبدیل" + }, + "earnings": { + "title": "درآمد", + "allTime": "کل", + "today": "امروز", + "week": "این هفته", + "month": "این ماه" + }, + "commission": { + "title": "کمیسیون", + "update": "به‌روزرسانی" + }, + "campaigns": { + "title": "کمپین‌های اختصاص‌یافته", + "noCampaigns": "کمپینی اختصاص نیافته", + "active": "فعال", + "inactive": "غیرفعال", + "assign": "اختصاص", + "unassign": "لغو اختصاص", + "createNew": "ایجاد", + "assignTitle": "اختصاص کمپین", + "noAvailable": "کمپینی برای اختصاص موجود نیست", + "createTitle": "ایجاد کمپین", + "createSubtitle": "کمپین به‌طور خودکار به شریک اختصاص می‌یابد", + "autoAssignNote": "کمپین به‌طور خودکار به شریک {{name}} اختصاص می‌یابد", + "createAndAssign": "ایجاد و اختصاص" + }, + "dangerZone": { + "title": "منطقه خطر", + "revokeButton": "لغو وضعیت شراکت" + }, + "commissionDialog": { + "title": "به‌روزرسانی کمیسیون", + "description": "درصد کمیسیون جدیدی برای این شریک تعیین کنید.", + "label": "کمیسیون (%)" + }, + "revokeDialog": { + "title": "لغو وضعیت شراکت؟", + "description": "این کار امتیازات شراکت را حذف می‌کند. شریک دیگر از معرفی‌ها کمیسیون دریافت نخواهد کرد." + } + }, "promocodes": { "title": "کدهای تخفیف", "subtitle": "مدیریت کدهای تخفیف و گروه‌های تخفیف", diff --git a/src/locales/ru.json b/src/locales/ru.json index 20cc607..29147d4 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -257,6 +257,15 @@ "servers_one": "{{count}} сервер", "servers_few": "{{count}} сервера", "servers_many": "{{count}} серверов", + "locationsLabel": "Локации", + "locations_one": "{{count}} локация", + "locations_few": "{{count}} локации", + "locations_many": "{{count}} локаций", + "trafficReset": { + "DAY": "Сброс ежедневно", + "WEEK": "Сброс еженедельно", + "MONTH": "Сброс ежемесячно" + }, "traffic": "Трафик", "unlimited": "Безлимит", "used": "Использовано", @@ -679,7 +688,83 @@ "referral_subscription_renewal": "Продление подписки реферала", "referral_bonus": "Бонус за реферала" }, - "disabled": "Реферальная программа временно отключена" + "disabled": "Реферальная программа временно отключена", + "partner": { + "becomePartner": "Стать партнером", + "becomePartnerDesc": "Подайте заявку на партнерскую программу, чтобы получить повышенную комиссию и возможность вывода заработка.", + "applyButton": "Подать заявку", + "applyTitle": "Заявка на партнерство", + "applyDesc": "Заполните форму ниже для подачи заявки на партнерскую программу. Все поля необязательные.", + "underReview": "Заявка на рассмотрении", + "underReviewDesc": "Ваша заявка на партнерство рассматривается. Мы уведомим вас, когда будет принято решение.", + "submittedAt": "Подана {{date}}", + "partnerStatus": "Партнер", + "active": "Активен", + "commissionInfo": "Ваша партнерская комиссия составляет {{percent}}%", + "rejected": "Заявка отклонена", + "reapplyButton": "Подать заново", + "applying": "Отправка...", + "submitApplication": "Отправить заявку", + "applyError": "Не удалось отправить заявку. Попробуйте снова.", + "yourCampaigns": "Ваши кампании", + "campaignLinks": { + "bot": "Ссылка в бот", + "web": "Ссылка в кабинет" + }, + "campaignBonus": { + "title": "Бонус для реферала", + "balance": "+{{amount}} на баланс", + "balanceDesc": "{{amount}} на баланс при регистрации", + "subscription": "{{days}} дн. подписки", + "subscriptionDesc": "{{days}} дней бесплатной подписки", + "tariff": "Бонусный тариф", + "tariffDesc": "Бесплатный тарифный план" + }, + "fields": { + "companyName": "Название компании", + "companyNamePlaceholder": "Название вашей компании или бренда", + "telegramChannel": "Telegram-канал", + "telegramChannelPlaceholder": "@вашканал", + "websiteUrl": "Сайт", + "websiteUrlPlaceholder": "https://example.com", + "description": "Описание", + "descriptionPlaceholder": "Расскажите о вашей аудитории и планах продвижения...", + "expectedReferrals": "Ожидаемое количество рефералов в месяц", + "expectedReferralsPlaceholder": "Примерное количество в месяц" + } + }, + "withdrawal": { + "title": "Вывод средств", + "goToWithdrawal": "Вывести", + "available": "Доступно", + "totalEarned": "Всего заработано", + "withdrawn": "Выведено", + "spent": "Потрачено на услуги", + "pending": "В обработке", + "requestButton": "Запросить вывод", + "minAmount": "Минимальная сумма вывода: {{amount}}", + "history": "История выводов", + "noHistory": "Запросов на вывод пока нет", + "requestTitle": "Запрос на вывод", + "requestDesc": "Доступно для вывода: {{available}}", + "requesting": "Отправка...", + "submitRequest": "Отправить запрос", + "requestError": "Не удалось создать запрос на вывод. Попробуйте снова.", + "fields": { + "amount": "Сумма (копейки)", + "amountPlaceholder": "Введите сумму в копейках", + "amountHint": "Введите сумму в копейках (100 копеек = 1 {{currency}})", + "paymentDetails": "Реквизиты для оплаты", + "paymentDetailsPlaceholder": "Номер карты, адрес кошелька или другие реквизиты..." + }, + "status": { + "pending": "В обработке", + "approved": "Одобрен", + "completed": "Завершен", + "rejected": "Отклонен", + "cancelled": "Отменен" + } + } }, "support": { "title": "Поддержка", @@ -796,7 +881,9 @@ "promoGroups": "Группы скидок", "trafficUsage": "Расход трафика", "updates": "Обновления", - "pinnedMessages": "Закреплённые" + "pinnedMessages": "Закреплённые", + "partners": "Партнёры", + "withdrawals": "Выводы" }, "panel": { "title": "Панель администратора", @@ -821,7 +908,9 @@ "promoGroupsDesc": "Группы скидок для пользователей", "trafficUsageDesc": "Трафик пользователей по нодам", "updatesDesc": "Версии бота и кабинета", - "pinnedMessagesDesc": "Управление закреплёнными сообщениями" + "pinnedMessagesDesc": "Управление закреплёнными сообщениями", + "partnersDesc": "Управление заявками партнёров и комиссиями", + "withdrawalsDesc": "Проверка и обработка заявок на вывод" }, "trafficUsage": { "title": "Расход трафика", @@ -1141,6 +1230,8 @@ "total": "Всего", "sent": "Отправлено", "failed": "Ошибки", + "blocked": "Заблокировали", + "blockedShort": "заблок.", "filter": "Фильтр", "message": "Сообщение", "unknownAdmin": "Неизвестный админ", @@ -2185,7 +2276,11 @@ "active": "Активна", "cancel": "Отмена", "save": "Сохранить", - "saving": "Сохранение..." + "saving": "Сохранение...", + "partner": "Партнёр", + "noPartner": "Без партнёра", + "selectPartner": "Выберите партнёра", + "partnerAutoAssign": "Кампания будет автоматически привязана к партнёру {{name}}" }, "stats": { "title": "Статистика кампании", @@ -2243,6 +2338,189 @@ "loadError": "Не удалось загрузить кампанию", "updateError": "Не удалось сохранить изменения" }, + "withdrawals": { + "title": "Выводы средств", + "subtitle": "Управление заявками на вывод", + "noData": "Нет заявок на вывод", + "risk": "Риск:", + "overview": { + "pendingCount": "Ожидают", + "pendingAmount": "Сумма ожидания", + "approvedCount": "Одобрено", + "completedCount": "Выполнено" + }, + "filter": { + "all": "Все", + "pending": "Ожидание", + "approved": "Одобрено", + "rejected": "Отклонено", + "completed": "Выполнено", + "cancelled": "Отменено" + }, + "status": { + "pending": "Ожидание", + "approved": "Одобрено", + "rejected": "Отклонено", + "completed": "Выполнено", + "cancelled": "Отменено", + "unknown": "Неизвестно" + }, + "detail": { + "title": "Заявка на вывод", + "loadError": "Не удалось загрузить детали заявки", + "userInfo": "Информация о пользователе", + "username": "Имя пользователя", + "telegramId": "Telegram ID", + "balance": "Баланс", + "totalReferrals": "Всего рефералов", + "totalEarnings": "Общий заработок", + "createdAt": "Создано", + "paymentDetails": "Реквизиты для оплаты", + "noPaymentDetails": "Реквизиты не указаны", + "riskAnalysis": "Анализ рисков", + "riskScore": "Оценка риска", + "riskLevel": { + "low": "Низкий", + "medium": "Средний", + "high": "Высокий", + "critical": "Критический" + }, + "flags": "Флаги", + "balanceStats": "Статистика баланса", + "referralDeposits": "Депозиты рефералов", + "suspiciousReferrals": "Подозрительные рефералы", + "earningsByReason": "Доходы по причинам", + "adminComment": "Комментарий администратора", + "processedAt": "Обработано", + "approve": "Одобрить", + "approving": "Одобрение...", + "reject": "Отклонить", + "rejecting": "Отклонение...", + "complete": "Отметить как выполнено", + "completing": "Завершение...", + "rejectTitle": "Отклонить заявку?", + "rejectDescription": "При необходимости добавьте комментарий с причиной отклонения.", + "commentPlaceholder": "Причина отклонения (необязательно)...", + "confirmReject": "Подтвердить отклонение" + } + }, + "partners": { + "title": "Партнёры", + "subtitle": "Управление партнёрами и заявками", + "totalPartners": "Всего партнёров", + "pendingApplications": "Заявок на рассмотрении", + "totalReferrals": "Всего рефералов", + "totalEarnings": "Всего заработано", + "tabs": { + "partners": "Партнёры", + "applications": "Заявки" + }, + "noPartners": "Партнёров пока нет", + "noApplications": "Нет заявок на рассмотрении", + "commission": "{{percent}}% комиссия", + "referrals": "{{count}} рефералов", + "actions": { + "approve": "Одобрить", + "reject": "Отклонить", + "review": "Рассмотреть" + }, + "applicationFields": { + "companyName": "Компания", + "website": "Сайт", + "channel": "Канал", + "description": "Описание", + "expectedReferrals": "Ожидаемых рефералов/мес" + }, + "approveDialog": { + "title": "Одобрить заявку", + "description": "Одобрить партнёрскую заявку от {{name}}", + "commissionLabel": "Комиссия (%)" + }, + "rejectDialog": { + "title": "Отклонить заявку", + "description": "Отклонить партнёрскую заявку от {{name}}", + "commentLabel": "Комментарий (необязательно)", + "commentPlaceholder": "Причина отклонения..." + }, + "settings": "Настройки", + "settingsSubtitle": "Настройки партнёрской программы и вывода средств", + "settingsLoadError": "Не удалось загрузить настройки", + "settingsUpdateError": "Не удалось сохранить настройки", + "settingsSection": { + "referralProgram": "Реферальная программа", + "withdrawalSettings": "Настройки вывода" + }, + "settingsFields": { + "programEnabled": "Реферальная программа включена", + "programEnabledDesc": "Вкл/выкл реферальную программу для всех пользователей", + "partnerVisible": "Раздел партнёрки виден в кабинете", + "partnerVisibleDesc": "Показывать/скрывать секцию подачи заявки на партнёрство и вывода средств", + "withdrawalEnabled": "Вывод средств включён", + "withdrawalEnabledDesc": "Разрешить партнёрам запрашивать вывод реферального баланса", + "minAmount": "Мин. сумма вывода (копейки)", + "minAmountDesc": "Минимальная сумма для запроса вывода (100 коп. = 1 ₽)", + "cooldownDays": "Кулдаун между выводами (дни)", + "cooldownDaysDesc": "Сколько дней должно пройти между запросами на вывод", + "requisitesText": "Текст для поля реквизитов", + "requisitesTextDesc": "Кастомная подсказка пользователю, какие реквизиты указать при выводе", + "requisitesTextPlaceholder": "Например: Укажите номер карты Сбербанк / Тинькофф или USDT TRC-20 адрес" + } + }, + "partnerDetail": { + "title": "Детали партнёра", + "loadError": "Не удалось загрузить данные партнёра", + "status": { + "approved": "Активен", + "pending": "На рассмотрении", + "rejected": "Отклонён", + "none": "Не партнёр" + }, + "stats": { + "totalReferrals": "Всего рефералов", + "paidReferrals": "Оплативших", + "activeReferrals": "Активных", + "conversionRate": "Конверсия" + }, + "earnings": { + "title": "Заработок", + "allTime": "За всё время", + "today": "Сегодня", + "week": "За неделю", + "month": "За месяц" + }, + "commission": { + "title": "Комиссия", + "update": "Изменить" + }, + "campaigns": { + "title": "Привязанные кампании", + "noCampaigns": "Нет привязанных кампаний", + "active": "Активна", + "inactive": "Неактивна", + "assign": "Привязать", + "unassign": "Отвязать", + "createNew": "Создать", + "assignTitle": "Привязать кампанию", + "noAvailable": "Нет доступных кампаний для привязки", + "createTitle": "Создать кампанию", + "createSubtitle": "Кампания будет автоматически привязана к партнёру", + "autoAssignNote": "Кампания будет автоматически привязана к партнёру {{name}}", + "createAndAssign": "Создать и привязать" + }, + "dangerZone": { + "title": "Опасная зона", + "revokeButton": "Отозвать партнёрский статус" + }, + "commissionDialog": { + "title": "Изменить комиссию", + "description": "Установите новый процент комиссии для этого партнёра.", + "label": "Комиссия (%)" + }, + "revokeDialog": { + "title": "Отозвать статус партнёра?", + "description": "Это лишит партнёра привилегий. Партнёр больше не будет получать комиссию от рефералов." + } + }, "promocodes": { "title": "Промокоды", "subtitle": "Управление промокодами и группами скидок", diff --git a/src/locales/zh.json b/src/locales/zh.json index 5e02d71..18dceb5 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -239,6 +239,13 @@ "baseTariff": "套餐", "serversLabel": "服务器", "servers_other": "{{count}} 个服务器", + "locationsLabel": "位置", + "locations_other": "{{count}} 个位置", + "trafficReset": { + "DAY": "每日重置", + "WEEK": "每周重置", + "MONTH": "每月重置" + }, "traffic": "流量", "unlimited": "无限", "used": "已使用", @@ -543,7 +550,83 @@ "referral_subscription_renewal": "推荐用户续订", "referral_bonus": "推荐奖金" }, - "disabled": "推荐计划暂时停用" + "disabled": "推荐计划暂时停用", + "partner": { + "becomePartner": "成为合作伙伴", + "becomePartnerDesc": "申请加入合作伙伴计划,获得更高的佣金率并可以提现您的收益。", + "applyButton": "申请", + "applyTitle": "合作伙伴申请", + "applyDesc": "填写以下表格申请我们的合作伙伴计划。所有字段均为可选。", + "underReview": "申请审核中", + "underReviewDesc": "您的合作伙伴申请正在审核中。我们会在做出决定后通知您。", + "submittedAt": "提交于 {{date}}", + "partnerStatus": "合作伙伴", + "active": "活跃", + "commissionInfo": "您的合作伙伴佣金率为 {{percent}}%", + "rejected": "申请被拒绝", + "reapplyButton": "重新申请", + "applying": "提交中...", + "submitApplication": "提交申请", + "applyError": "提交申请失败,请重试。", + "yourCampaigns": "您的活动", + "campaignLinks": { + "bot": "机器人链接", + "web": "网页链接" + }, + "campaignBonus": { + "title": "推荐奖励", + "balance": "+{{amount}} 余额", + "balanceDesc": "注册时充值 {{amount}}", + "subscription": "{{days}} 天订阅", + "subscriptionDesc": "{{days}} 天免费订阅", + "tariff": "奖励套餐", + "tariffDesc": "免费套餐方案" + }, + "fields": { + "companyName": "公司名称", + "companyNamePlaceholder": "您的公司或品牌名称", + "telegramChannel": "Telegram频道", + "telegramChannelPlaceholder": "@yourchannel", + "websiteUrl": "网站地址", + "websiteUrlPlaceholder": "https://example.com", + "description": "描述", + "descriptionPlaceholder": "请介绍您的受众以及推广计划...", + "expectedReferrals": "预计每月推荐数", + "expectedReferralsPlaceholder": "每月预估数量" + } + }, + "withdrawal": { + "title": "提现", + "goToWithdrawal": "提现", + "available": "可提现", + "totalEarned": "总收益", + "withdrawn": "已提现", + "spent": "已用于服务", + "pending": "待处理", + "requestButton": "申请提现", + "minAmount": "最低提现金额:{{amount}}", + "history": "提现记录", + "noHistory": "暂无提现记录", + "requestTitle": "申请提现", + "requestDesc": "可提现金额:{{available}}", + "requesting": "提交中...", + "submitRequest": "提交请求", + "requestError": "创建提现请求失败,请重试。", + "fields": { + "amount": "金额(戈比)", + "amountPlaceholder": "输入金额(戈比)", + "amountHint": "请输入戈比金额(100戈比 = 1 {{currency}})", + "paymentDetails": "付款信息", + "paymentDetailsPlaceholder": "银行卡号、钱包地址或其他付款信息..." + }, + "status": { + "pending": "待处理", + "approved": "已批准", + "completed": "已完成", + "rejected": "已拒绝", + "cancelled": "已取消" + } + } }, "support": { "title": "支持", @@ -656,7 +739,9 @@ "users": "用户", "trafficUsage": "流量使用", "updates": "更新", - "pinnedMessages": "置顶消息" + "pinnedMessages": "置顶消息", + "partners": "合作伙伴", + "withdrawals": "提现" }, "panel": { "title": "管理面板", @@ -681,7 +766,9 @@ "usersDesc": "管理机器人用户", "trafficUsageDesc": "按节点统计用户流量", "updatesDesc": "机器人和面板版本", - "pinnedMessagesDesc": "管理用户置顶消息" + "pinnedMessagesDesc": "管理用户置顶消息", + "partnersDesc": "管理合作伙伴申请和佣金", + "withdrawalsDesc": "审核和处理提现请求" }, "trafficUsage": { "title": "流量使用", @@ -889,6 +976,8 @@ "total": "总计", "sent": "已发送", "failed": "失败", + "blocked": "已屏蔽", + "blockedShort": "屏蔽", "filter": "筛选", "message": "消息", "unknownAdmin": "未知", @@ -1403,7 +1492,11 @@ "active": "启用", "cancel": "取消", "save": "保存", - "saving": "保存中..." + "saving": "保存中...", + "partner": "合作伙伴", + "noPartner": "无合作伙伴", + "selectPartner": "选择合作伙伴", + "partnerAutoAssign": "活动将自动分配给合作伙伴 {{name}}" }, "stats": { "title": "活动统计", @@ -1457,6 +1550,165 @@ "loadError": "加载活动失败", "updateError": "保存更改失败" }, + "withdrawals": { + "title": "提现", + "subtitle": "管理提现请求", + "noData": "暂无提现请求", + "risk": "风险:", + "overview": { + "pendingCount": "待处理", + "pendingAmount": "待处理金额", + "approvedCount": "已批准", + "completedCount": "已完成" + }, + "filter": { + "all": "全部", + "pending": "待处理", + "approved": "已批准", + "rejected": "已拒绝", + "completed": "已完成", + "cancelled": "已取消" + }, + "status": { + "pending": "待处理", + "approved": "已批准", + "rejected": "已拒绝", + "completed": "已完成", + "cancelled": "已取消" + }, + "detail": { + "title": "提现详情", + "loadError": "加载提现详情失败", + "userInfo": "用户信息", + "username": "用户名", + "telegramId": "Telegram ID", + "balance": "余额", + "totalReferrals": "总推荐数", + "totalEarnings": "总收益", + "createdAt": "创建时间", + "paymentDetails": "付款信息", + "noPaymentDetails": "未提供付款信息", + "riskAnalysis": "风险分析", + "riskScore": "风险评分", + "riskLevel": { + "low": "低", + "medium": "中", + "high": "高", + "critical": "严重" + }, + "flags": "标记", + "balanceStats": "余额统计", + "referralDeposits": "推荐充值", + "suspiciousReferrals": "可疑推荐", + "earningsByReason": "按原因分类收益", + "adminComment": "管理员备注", + "processedAt": "处理时间", + "approve": "批准", + "approving": "批准中...", + "reject": "拒绝", + "rejecting": "拒绝中...", + "complete": "标记为已完成", + "completing": "完成中...", + "rejectTitle": "拒绝提现?", + "rejectDescription": "可选择添加备注说明拒绝原因。", + "commentPlaceholder": "拒绝原因(可选)...", + "confirmReject": "确认拒绝" + } + }, + "partners": { + "title": "合作伙伴", + "subtitle": "合作伙伴管理和申请", + "totalPartners": "总合作伙伴数", + "pendingApplications": "待审核申请", + "totalReferrals": "总推荐数", + "totalEarnings": "总收益", + "tabs": { + "partners": "合作伙伴", + "applications": "申请" + }, + "noPartners": "暂无合作伙伴", + "noApplications": "暂无待审核申请", + "commission": "{{percent}}% 佣金", + "referrals": "{{count}} 个推荐", + "actions": { + "approve": "批准", + "reject": "拒绝", + "review": "审核" + }, + "applicationFields": { + "companyName": "公司", + "website": "网站", + "channel": "频道", + "description": "描述", + "expectedReferrals": "预计每月推荐数" + }, + "approveDialog": { + "title": "批准申请", + "description": "批准来自 {{name}} 的合作伙伴申请", + "commissionLabel": "佣金(%)" + }, + "rejectDialog": { + "title": "拒绝申请", + "description": "拒绝来自 {{name}} 的合作伙伴申请", + "commentLabel": "备注(可选)", + "commentPlaceholder": "拒绝原因..." + } + }, + "partnerDetail": { + "title": "合作伙伴详情", + "loadError": "加载合作伙伴详情失败", + "status": { + "approved": "活跃", + "pending": "审核中", + "rejected": "已拒绝", + "none": "非合作伙伴" + }, + "stats": { + "totalReferrals": "总推荐数", + "paidReferrals": "已付费推荐", + "activeReferrals": "活跃推荐", + "conversionRate": "转化率" + }, + "earnings": { + "title": "收益", + "allTime": "总计", + "today": "今日", + "week": "本周", + "month": "本月" + }, + "commission": { + "title": "佣金", + "update": "更新" + }, + "campaigns": { + "title": "分配的活动", + "noCampaigns": "暂无分配的活动", + "active": "活跃", + "inactive": "未激活", + "assign": "分配", + "unassign": "取消分配", + "createNew": "创建", + "assignTitle": "分配活动", + "noAvailable": "没有可分配的活动", + "createTitle": "创建活动", + "createSubtitle": "活动将自动分配给合作伙伴", + "autoAssignNote": "活动将自动分配给合作伙伴 {{name}}", + "createAndAssign": "创建并分配" + }, + "dangerZone": { + "title": "危险操作", + "revokeButton": "撤销合作伙伴身份" + }, + "commissionDialog": { + "title": "更新佣金", + "description": "为该合作伙伴设置新的佣金百分比。", + "label": "佣金(%)" + }, + "revokeDialog": { + "title": "撤销合作伙伴身份?", + "description": "这将移除合作伙伴权限。该合作伙伴将不再从推荐中获得佣金。" + } + }, "promocodes": { "title": "促销码", "subtitle": "管理促销码和折扣组", diff --git a/src/pages/AdminApplicationReview.tsx b/src/pages/AdminApplicationReview.tsx new file mode 100644 index 0000000..35f83e5 --- /dev/null +++ b/src/pages/AdminApplicationReview.tsx @@ -0,0 +1,228 @@ +import { useState } from 'react'; +import { useParams, useNavigate, useLocation } from 'react-router'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { partnerApi, type AdminPartnerApplicationItem } from '../api/partners'; +import { AdminBackButton } from '../components/admin'; + +export default function AdminApplicationReview() { + const { t } = useTranslation(); + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + const queryClient = useQueryClient(); + + const [commission, setCommission] = useState('10'); + const [rejectComment, setRejectComment] = useState(''); + + // Try to get application from navigate state, fallback to fetching + const passedApp = (location.state as { application?: AdminPartnerApplicationItem } | null) + ?.application; + + const { data: fetchedApps } = useQuery({ + queryKey: ['admin-partner-applications'], + queryFn: () => partnerApi.getApplications({ status: 'pending' }), + enabled: !passedApp && !!id, + }); + + const app = passedApp ?? fetchedApps?.items.find((a) => a.id === Number(id)); + + const approveMutation = useMutation({ + mutationFn: ({ appId, commissionPercent }: { appId: number; commissionPercent: number }) => + partnerApi.approveApplication(appId, { commission_percent: commissionPercent }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-partner-applications'] }); + queryClient.invalidateQueries({ queryKey: ['admin-partners'] }); + queryClient.invalidateQueries({ queryKey: ['admin-partner-stats'] }); + navigate('/admin/partners'); + }, + }); + + const rejectMutation = useMutation({ + mutationFn: ({ appId, comment }: { appId: number; comment?: string }) => + partnerApi.rejectApplication(appId, { comment }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-partner-applications'] }); + queryClient.invalidateQueries({ queryKey: ['admin-partner-stats'] }); + navigate('/admin/partners'); + }, + }); + + if (!app) { + return ( +
+
+ +

+ {t('admin.partners.approveDialog.title')} +

+
+
+
+
+
+ ); + } + + const displayName = app.first_name || app.username || `#${app.user_id}`; + + return ( +
+ {/* Header */} +
+ +
+

+ {t('admin.partners.actions.review')} +

+

{displayName}

+
+
+ +
+ {/* Application Details */} +
+

+ {t('admin.partners.tabs.applications')} +

+
+
+
+
+ {displayName} + {app.username && @{app.username}} +
+
+
+ +
+ {app.company_name && ( +
+ + {t('admin.partners.applicationFields.companyName')}: + {' '} + {app.company_name} +
+ )} + {app.website_url && ( +
+ + {t('admin.partners.applicationFields.website')}: + {' '} + {app.website_url} +
+ )} + {app.telegram_channel && ( +
+ + {t('admin.partners.applicationFields.channel')}: + {' '} + {app.telegram_channel} +
+ )} + {app.description && ( +
+ + {t('admin.partners.applicationFields.description')}: + {' '} + {app.description} +
+ )} + {app.expected_monthly_referrals != null && ( +
+ + {t('admin.partners.applicationFields.expectedReferrals')}: + {' '} + {app.expected_monthly_referrals} +
+ )} +
+
+
+ + {/* Approve Section */} +
+

+ {t('admin.partners.approveDialog.title')} +

+

+ {t('admin.partners.approveDialog.description', { name: displayName })} +

+ + setCommission(e.target.value)} + className="mb-4 w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 outline-none focus:border-accent-500" + placeholder="10" + /> + +
+ + {approveMutation.isError && ( +
+ {t('common.error')} +
+ )} + + {/* Reject Section */} +
+

+ {t('admin.partners.rejectDialog.title')} +

+

+ {t('admin.partners.rejectDialog.description', { name: displayName })} +

+ +