diff --git a/src/App.tsx b/src/App.tsx index f98b65d..29b455b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -60,6 +60,7 @@ 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')); @@ -591,6 +592,16 @@ function App() { } /> + + + + + + } + /> => { @@ -191,4 +211,18 @@ export const partnerApi = { 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 index 6e0dfe8..f7c385b 100644 --- a/src/api/withdrawals.ts +++ b/src/api/withdrawals.ts @@ -14,6 +14,7 @@ export interface WithdrawalBalanceResponse { is_withdrawal_enabled: boolean; can_request: boolean; cannot_request_reason: string | null; + requisites_text: string; } export interface WithdrawalCreateRequest { diff --git a/src/locales/en.json b/src/locales/en.json index 864a271..d9e6ab8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1906,6 +1906,29 @@ "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": { diff --git a/src/locales/ru.json b/src/locales/ru.json index ee88b1e..18e3075 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2428,6 +2428,29 @@ "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": { diff --git a/src/pages/AdminPartnerSettings.tsx b/src/pages/AdminPartnerSettings.tsx new file mode 100644 index 0000000..319ba8e --- /dev/null +++ b/src/pages/AdminPartnerSettings.tsx @@ -0,0 +1,318 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { partnerApi } from '../api/partners'; +import { AdminBackButton } from '../components/admin'; +import { toNumber } from '../utils/inputHelpers'; + +type NumberOrEmpty = number | ''; + +const SettingsIcon = () => ( + + + + +); + +export default function AdminPartnerSettings() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const { + data: settings, + isLoading, + error, + } = useQuery({ + queryKey: ['partner-settings'], + queryFn: partnerApi.getPartnerSettings, + }); + + const [formData, setFormData] = useState<{ + referral_program_enabled: boolean; + partner_section_visible: boolean; + withdrawal_enabled: boolean; + withdrawal_min_amount_kopeks: NumberOrEmpty; + withdrawal_cooldown_days: NumberOrEmpty; + withdrawal_requisites_text: string; + }>({ + referral_program_enabled: true, + partner_section_visible: true, + withdrawal_enabled: false, + withdrawal_min_amount_kopeks: 100000, + withdrawal_cooldown_days: 30, + withdrawal_requisites_text: '', + }); + + useEffect(() => { + if (settings) { + setFormData({ + referral_program_enabled: settings.referral_program_enabled, + partner_section_visible: settings.partner_section_visible, + withdrawal_enabled: settings.withdrawal_enabled, + withdrawal_min_amount_kopeks: settings.withdrawal_min_amount_kopeks, + withdrawal_cooldown_days: settings.withdrawal_cooldown_days, + withdrawal_requisites_text: settings.withdrawal_requisites_text, + }); + } + }, [settings]); + + const updateMutation = useMutation({ + mutationFn: partnerApi.updatePartnerSettings, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['partner-settings'] }); + queryClient.invalidateQueries({ queryKey: ['referral-terms'] }); + navigate('/admin/partners'); + }, + }); + + // Validation + const isMinAmountValid = + formData.withdrawal_min_amount_kopeks !== '' && + formData.withdrawal_min_amount_kopeks >= 0 && + formData.withdrawal_min_amount_kopeks <= 100_000_000; + const isCooldownValid = + formData.withdrawal_cooldown_days !== '' && + formData.withdrawal_cooldown_days >= 0 && + formData.withdrawal_cooldown_days <= 365; + const isValid = !formData.withdrawal_enabled || (isMinAmountValid && isCooldownValid); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!isValid) return; + updateMutation.mutate({ + ...formData, + withdrawal_min_amount_kopeks: toNumber(formData.withdrawal_min_amount_kopeks, 100000), + withdrawal_cooldown_days: toNumber(formData.withdrawal_cooldown_days, 30), + }); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
+ +

{t('admin.partners.settings')}

+
+
+

{t('admin.partners.settingsLoadError')}

+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+ +
+
+

{t('admin.partners.settings')}

+

{t('admin.partners.settingsSubtitle')}

+
+
+ +
+ {/* Referral Program Section */} +
+

+ {t('admin.partners.settingsSection.referralProgram')} +

+ + {/* Program Enabled */} +
+ +
+ + {/* Partner Section Visible */} +
+ +
+
+ + {/* Withdrawal Settings Section */} +
+

+ {t('admin.partners.settingsSection.withdrawalSettings')} +

+ + {/* Withdrawal Enabled */} +
+ +
+ + {/* Min Amount */} +
+ + { + const val = e.target.value; + if (val === '') + return setFormData({ ...formData, withdrawal_min_amount_kopeks: '' }); + const num = parseInt(val); + if (!isNaN(num)) setFormData({ ...formData, withdrawal_min_amount_kopeks: num }); + }} + className="input" + disabled={!formData.withdrawal_enabled} + /> +

+ {t('admin.partners.settingsFields.minAmountDesc')} +

+
+ + {/* Cooldown Days */} +
+ + { + const val = e.target.value; + if (val === '') return setFormData({ ...formData, withdrawal_cooldown_days: '' }); + const num = parseInt(val); + if (!isNaN(num)) setFormData({ ...formData, withdrawal_cooldown_days: num }); + }} + className="input" + disabled={!formData.withdrawal_enabled} + /> +

+ {t('admin.partners.settingsFields.cooldownDaysDesc')} +

+
+ + {/* Requisites Text */} +
+ +