mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add Referrals tab to admin user detail page
- New Рефералы tab with three sections: - Referred By: shows who referred this user, assign/remove referrer - Stats grid: total referrals, earnings, commission, referral code - Referrals list: all referred users with navigate and remove buttons - User search with debounce for assigning referrer - Shows warning when selected user already has a referrer - API methods: assignReferrer, removeReferrer, removeReferral - i18n translations for all 4 locales (en, ru, zh, fa)
This commit is contained in:
@@ -536,6 +536,34 @@ export const adminUsersApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Assign a referrer to this user
|
||||||
|
assignReferrer: async (
|
||||||
|
userId: number,
|
||||||
|
referrerId: number,
|
||||||
|
): Promise<{ success: boolean; message: string }> => {
|
||||||
|
const response = await apiClient.post(`/cabinet/admin/users/${userId}/assign-referrer`, {
|
||||||
|
referrer_id: referrerId,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove this user's referrer
|
||||||
|
removeReferrer: async (userId: number): Promise<{ success: boolean; message: string }> => {
|
||||||
|
const response = await apiClient.delete(`/cabinet/admin/users/${userId}/referrer`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove a specific referral from this user's list
|
||||||
|
removeReferral: async (
|
||||||
|
userId: number,
|
||||||
|
referralUserId: number,
|
||||||
|
): Promise<{ success: boolean; message: string }> => {
|
||||||
|
const response = await apiClient.delete(
|
||||||
|
`/cabinet/admin/users/${userId}/referrals/${referralUserId}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// Get referrals
|
// Get referrals
|
||||||
getReferrals: async (userId: number, offset = 0, limit = 50): Promise<UsersListResponse> => {
|
getReferrals: async (userId: number, offset = 0, limit = 50): Promise<UsersListResponse> => {
|
||||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, {
|
const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, {
|
||||||
|
|||||||
@@ -2940,7 +2940,8 @@
|
|||||||
"balance": "Balance",
|
"balance": "Balance",
|
||||||
"sync": "Synchronization",
|
"sync": "Synchronization",
|
||||||
"tickets": "Tickets",
|
"tickets": "Tickets",
|
||||||
"gifts": "Gifts"
|
"gifts": "Gifts",
|
||||||
|
"referrals": "Referrals"
|
||||||
},
|
},
|
||||||
"noTickets": "No tickets from this user",
|
"noTickets": "No tickets from this user",
|
||||||
"ticketsCount": "tickets",
|
"ticketsCount": "tickets",
|
||||||
@@ -3100,6 +3101,26 @@
|
|||||||
"unknownUser": "Unknown",
|
"unknownUser": "Unknown",
|
||||||
"totalSent": "Total sent",
|
"totalSent": "Total sent",
|
||||||
"totalReceived": "Total received"
|
"totalReceived": "Total received"
|
||||||
|
},
|
||||||
|
"referrals": {
|
||||||
|
"referredBy": "Referred By",
|
||||||
|
"noReferrer": "No referrer assigned",
|
||||||
|
"assignReferrer": "Assign Referrer",
|
||||||
|
"removeReferrer": "Remove",
|
||||||
|
"referrerAssigned": "Referrer assigned successfully",
|
||||||
|
"referrerRemoved": "Referrer removed successfully",
|
||||||
|
"totalReferrals": "Total Referrals",
|
||||||
|
"totalEarnings": "Total Earnings",
|
||||||
|
"commission": "Commission",
|
||||||
|
"referralCode": "Referral Code",
|
||||||
|
"default": "default",
|
||||||
|
"referralsList": "Referrals",
|
||||||
|
"noReferrals": "No referrals yet",
|
||||||
|
"removeReferral": "Remove referral",
|
||||||
|
"referralRemoved": "Referral removed successfully",
|
||||||
|
"searchPlaceholder": "Search by name, email, or Telegram ID...",
|
||||||
|
"alreadyReferred": "already has referrer",
|
||||||
|
"noUsersFound": "No users found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2562,7 +2562,8 @@
|
|||||||
"balance": "موجودی",
|
"balance": "موجودی",
|
||||||
"sync": "همگامسازی",
|
"sync": "همگامسازی",
|
||||||
"tickets": "تیکتها",
|
"tickets": "تیکتها",
|
||||||
"gifts": "هدایا"
|
"gifts": "هدایا",
|
||||||
|
"referrals": "معرفیها"
|
||||||
},
|
},
|
||||||
"noTickets": "این کاربر تیکتی ندارد",
|
"noTickets": "این کاربر تیکتی ندارد",
|
||||||
"ticketsCount": "تیکت",
|
"ticketsCount": "تیکت",
|
||||||
@@ -2718,6 +2719,26 @@
|
|||||||
"unknownUser": "ناشناس",
|
"unknownUser": "ناشناس",
|
||||||
"totalSent": "کل ارسال شده",
|
"totalSent": "کل ارسال شده",
|
||||||
"totalReceived": "کل دریافت شده"
|
"totalReceived": "کل دریافت شده"
|
||||||
|
},
|
||||||
|
"referrals": {
|
||||||
|
"referredBy": "معرفیشده توسط",
|
||||||
|
"noReferrer": "معرفیکنندهای تعیین نشده",
|
||||||
|
"assignReferrer": "تعیین معرفیکننده",
|
||||||
|
"removeReferrer": "حذف",
|
||||||
|
"referrerAssigned": "معرفیکننده تعیین شد",
|
||||||
|
"referrerRemoved": "معرفیکننده حذف شد",
|
||||||
|
"totalReferrals": "کل معرفیها",
|
||||||
|
"totalEarnings": "کل درآمد",
|
||||||
|
"commission": "کمیسیون",
|
||||||
|
"referralCode": "کد معرفی",
|
||||||
|
"default": "پیشفرض",
|
||||||
|
"referralsList": "لیست معرفیها",
|
||||||
|
"noReferrals": "هنوز معرفیای نیست",
|
||||||
|
"removeReferral": "حذف معرفی",
|
||||||
|
"referralRemoved": "معرفی حذف شد",
|
||||||
|
"searchPlaceholder": "جستجو بر اساس نام، ایمیل یا شناسه تلگرام...",
|
||||||
|
"alreadyReferred": "قبلاً معرفی شده",
|
||||||
|
"noUsersFound": "کاربری یافت نشد"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3452,7 +3452,8 @@
|
|||||||
"balance": "Баланс",
|
"balance": "Баланс",
|
||||||
"sync": "Синхронизация",
|
"sync": "Синхронизация",
|
||||||
"tickets": "Тикеты",
|
"tickets": "Тикеты",
|
||||||
"gifts": "Подарки"
|
"gifts": "Подарки",
|
||||||
|
"referrals": "Рефералы"
|
||||||
},
|
},
|
||||||
"noTickets": "У пользователя нет тикетов",
|
"noTickets": "У пользователя нет тикетов",
|
||||||
"ticketsCount": "тикетов",
|
"ticketsCount": "тикетов",
|
||||||
@@ -3611,6 +3612,26 @@
|
|||||||
"unknownUser": "Неизвестный",
|
"unknownUser": "Неизвестный",
|
||||||
"totalSent": "Всего отправлено",
|
"totalSent": "Всего отправлено",
|
||||||
"totalReceived": "Всего получено"
|
"totalReceived": "Всего получено"
|
||||||
|
},
|
||||||
|
"referrals": {
|
||||||
|
"referredBy": "Приглашён",
|
||||||
|
"noReferrer": "Реферер не назначен",
|
||||||
|
"assignReferrer": "Назначить реферера",
|
||||||
|
"removeReferrer": "Удалить",
|
||||||
|
"referrerAssigned": "Реферер назначен",
|
||||||
|
"referrerRemoved": "Реферер удалён",
|
||||||
|
"totalReferrals": "Всего рефералов",
|
||||||
|
"totalEarnings": "Всего заработано",
|
||||||
|
"commission": "Комиссия",
|
||||||
|
"referralCode": "Реферальный код",
|
||||||
|
"default": "стандарт",
|
||||||
|
"referralsList": "Рефералы",
|
||||||
|
"noReferrals": "Рефералов пока нет",
|
||||||
|
"removeReferral": "Удалить реферала",
|
||||||
|
"referralRemoved": "Реферал удалён",
|
||||||
|
"searchPlaceholder": "Поиск по имени, email или Telegram ID...",
|
||||||
|
"alreadyReferred": "уже чей-то реферал",
|
||||||
|
"noUsersFound": "Пользователи не найдены"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2561,7 +2561,8 @@
|
|||||||
"balance": "余额",
|
"balance": "余额",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"tickets": "工单",
|
"tickets": "工单",
|
||||||
"gifts": "礼物"
|
"gifts": "礼物",
|
||||||
|
"referrals": "推荐"
|
||||||
},
|
},
|
||||||
"noTickets": "该用户没有工单",
|
"noTickets": "该用户没有工单",
|
||||||
"ticketsCount": "个工单",
|
"ticketsCount": "个工单",
|
||||||
@@ -2717,6 +2718,26 @@
|
|||||||
"unknownUser": "未知",
|
"unknownUser": "未知",
|
||||||
"totalSent": "已发送总数",
|
"totalSent": "已发送总数",
|
||||||
"totalReceived": "已收到总数"
|
"totalReceived": "已收到总数"
|
||||||
|
},
|
||||||
|
"referrals": {
|
||||||
|
"referredBy": "推荐人",
|
||||||
|
"noReferrer": "未分配推荐人",
|
||||||
|
"assignReferrer": "分配推荐人",
|
||||||
|
"removeReferrer": "移除",
|
||||||
|
"referrerAssigned": "推荐人已分配",
|
||||||
|
"referrerRemoved": "推荐人已移除",
|
||||||
|
"totalReferrals": "总推荐数",
|
||||||
|
"totalEarnings": "总收益",
|
||||||
|
"commission": "佣金",
|
||||||
|
"referralCode": "推荐码",
|
||||||
|
"default": "默认",
|
||||||
|
"referralsList": "推荐列表",
|
||||||
|
"noReferrals": "暂无推荐",
|
||||||
|
"removeReferral": "移除推荐",
|
||||||
|
"referralRemoved": "推荐已移除",
|
||||||
|
"searchPlaceholder": "按名称、邮箱或 Telegram ID 搜索...",
|
||||||
|
"alreadyReferred": "已有推荐人",
|
||||||
|
"noUsersFound": "未找到用户"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export default function AdminUserDetail() {
|
|||||||
const [user, setUser] = useState<UserDetailResponse | null>(null);
|
const [user, setUser] = useState<UserDetailResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
'info' | 'subscription' | 'balance' | 'sync' | 'tickets' | 'gifts'
|
'info' | 'subscription' | 'balance' | 'sync' | 'tickets' | 'gifts' | 'referrals'
|
||||||
>('info');
|
>('info');
|
||||||
const [syncStatus, setSyncStatus] = useState<PanelSyncStatusResponse | null>(null);
|
const [syncStatus, setSyncStatus] = useState<PanelSyncStatusResponse | null>(null);
|
||||||
const [tariffs, setTariffs] = useState<UserAvailableTariff[]>([]);
|
const [tariffs, setTariffs] = useState<UserAvailableTariff[]>([]);
|
||||||
@@ -306,6 +306,15 @@ export default function AdminUserDetail() {
|
|||||||
const [referrals, setReferrals] = useState<UserListItem[]>([]);
|
const [referrals, setReferrals] = useState<UserListItem[]>([]);
|
||||||
const [referralsLoading, setReferralsLoading] = useState(false);
|
const [referralsLoading, setReferralsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Referrals tab state
|
||||||
|
const [referralsList, setReferralsList] = useState<UserListItem[]>([]);
|
||||||
|
const [referralsTotal, setReferralsTotal] = useState(0);
|
||||||
|
const [referralsListLoading, setReferralsListLoading] = useState(false);
|
||||||
|
const [referrerSearchQuery, setReferrerSearchQuery] = useState('');
|
||||||
|
const [showReferrerSearch, setShowReferrerSearch] = useState(false);
|
||||||
|
const [referrerSearchResults, setReferrerSearchResults] = useState<UserListItem[]>([]);
|
||||||
|
const referrerSearchRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Panel info & node usage
|
// Panel info & node usage
|
||||||
const [panelInfo, setPanelInfo] = useState<UserPanelInfo | null>(null);
|
const [panelInfo, setPanelInfo] = useState<UserPanelInfo | null>(null);
|
||||||
const [panelInfoLoading, setPanelInfoLoading] = useState(false);
|
const [panelInfoLoading, setPanelInfoLoading] = useState(false);
|
||||||
@@ -441,6 +450,20 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
|
const loadReferralsList = useCallback(async () => {
|
||||||
|
if (!userId) return;
|
||||||
|
setReferralsListLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await adminUsersApi.getReferrals(userId, 0, 100);
|
||||||
|
setReferralsList(data.users || []);
|
||||||
|
setReferralsTotal(data.total || 0);
|
||||||
|
} catch {
|
||||||
|
// silent
|
||||||
|
} finally {
|
||||||
|
setReferralsListLoading(false);
|
||||||
|
}
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
const loadPanelInfo = useCallback(async () => {
|
const loadPanelInfo = useCallback(async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
try {
|
try {
|
||||||
@@ -559,6 +582,7 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
if (activeTab === 'tickets') loadTickets();
|
if (activeTab === 'tickets') loadTickets();
|
||||||
if (activeTab === 'gifts') loadGifts();
|
if (activeTab === 'gifts') loadGifts();
|
||||||
|
if (activeTab === 'referrals') loadReferralsList();
|
||||||
}, [
|
}, [
|
||||||
activeTab,
|
activeTab,
|
||||||
loadSyncStatus,
|
loadSyncStatus,
|
||||||
@@ -568,6 +592,7 @@ export default function AdminUserDetail() {
|
|||||||
loadSubscriptionData,
|
loadSubscriptionData,
|
||||||
loadPromoGroups,
|
loadPromoGroups,
|
||||||
loadGifts,
|
loadGifts,
|
||||||
|
loadReferralsList,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -872,6 +897,90 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Referrals tab: assign referrer
|
||||||
|
const handleAssignReferrer = async (referrerId: number) => {
|
||||||
|
if (!userId) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await adminUsersApi.assignReferrer(userId, referrerId);
|
||||||
|
await loadUser();
|
||||||
|
setShowReferrerSearch(false);
|
||||||
|
setReferrerSearchQuery('');
|
||||||
|
setReferrerSearchResults([]);
|
||||||
|
notify.success(t('admin.users.detail.referrals.referrerAssigned'));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
||||||
|
notify.error(axiosErr?.response?.data?.detail || t('common.error'));
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Referrals tab: remove referrer
|
||||||
|
const handleRemoveReferrer = async () => {
|
||||||
|
if (!userId) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await adminUsersApi.removeReferrer(userId);
|
||||||
|
await loadUser();
|
||||||
|
notify.success(t('admin.users.detail.referrals.referrerRemoved'));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
||||||
|
notify.error(axiosErr?.response?.data?.detail || t('common.error'));
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Referrals tab: remove a referral
|
||||||
|
const handleRemoveReferral = async (referralUserId: number) => {
|
||||||
|
if (!userId) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await adminUsersApi.removeReferral(userId, referralUserId);
|
||||||
|
await loadReferralsList();
|
||||||
|
await loadUser();
|
||||||
|
notify.success(t('admin.users.detail.referrals.referralRemoved'));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
||||||
|
notify.error(axiosErr?.response?.data?.detail || t('common.error'));
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Referrals tab: debounced user search for referrer assignment
|
||||||
|
useEffect(() => {
|
||||||
|
if (referrerSearchQuery.length < 2 || !showReferrerSearch) {
|
||||||
|
setReferrerSearchResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const data = await adminUsersApi.getUsers({ search: referrerSearchQuery, limit: 10 });
|
||||||
|
setReferrerSearchResults(data.users || []);
|
||||||
|
} catch {
|
||||||
|
setReferrerSearchResults([]);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [referrerSearchQuery, showReferrerSearch]);
|
||||||
|
|
||||||
|
// Referrals tab: close search dropdown on click outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (referrerSearchRef.current && !referrerSearchRef.current.contains(e.target as Node)) {
|
||||||
|
setShowReferrerSearch(false);
|
||||||
|
setReferrerSearchQuery('');
|
||||||
|
setReferrerSearchResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (showReferrerSearch) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [showReferrerSearch]);
|
||||||
|
|
||||||
const handleResetTrial = async () => {
|
const handleResetTrial = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
@@ -1033,7 +1142,7 @@ export default function AdminUserDetail() {
|
|||||||
className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1"
|
className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1"
|
||||||
style={{ WebkitOverflowScrolling: 'touch' }}
|
style={{ WebkitOverflowScrolling: 'touch' }}
|
||||||
>
|
>
|
||||||
{(['info', 'subscription', 'balance', 'sync', 'tickets', 'gifts'] as const)
|
{(['info', 'subscription', 'balance', 'sync', 'tickets', 'gifts', 'referrals'] as const)
|
||||||
.filter((tab) => tab !== 'sync' || hasPermission('users:sync'))
|
.filter((tab) => tab !== 'sync' || hasPermission('users:sync'))
|
||||||
.map((tab) => (
|
.map((tab) => (
|
||||||
<button
|
<button
|
||||||
@@ -1051,6 +1160,7 @@ export default function AdminUserDetail() {
|
|||||||
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
|
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
|
||||||
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
|
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
|
||||||
{tab === 'gifts' && t('admin.users.detail.tabs.gifts')}
|
{tab === 'gifts' && t('admin.users.detail.tabs.gifts')}
|
||||||
|
{tab === 'referrals' && t('admin.users.detail.tabs.referrals')}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2863,6 +2973,222 @@ export default function AdminUserDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Referrals Tab */}
|
||||||
|
{activeTab === 'referrals' && user && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Section 1: Who referred this user */}
|
||||||
|
<div className="rounded-2xl border border-dark-700/30 bg-dark-800/40 p-5">
|
||||||
|
<h3 className="mb-4 text-base font-semibold text-dark-100">
|
||||||
|
{t('admin.users.detail.referrals.referredBy')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{user.referral.referred_by_id ? (
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/admin/users/${user.referral.referred_by_id}`)}
|
||||||
|
className="flex items-center gap-3 rounded-xl bg-dark-700/30 px-4 py-3 transition-colors hover:bg-dark-700/50"
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-accent-500/20 text-sm font-bold text-accent-400">
|
||||||
|
{(user.referral.referred_by_username || '?')[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-dark-100">
|
||||||
|
{user.referral.referred_by_username ||
|
||||||
|
`ID: ${user.referral.referred_by_id}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
ID: {user.referral.referred_by_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveReferrer}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="rounded-lg border border-error-500/30 bg-error-500/10 px-3 py-2 text-sm text-error-400 transition-colors hover:bg-error-500/20 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t('admin.users.detail.referrals.removeReferrer')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{showReferrerSearch ? (
|
||||||
|
<div ref={referrerSearchRef} className="relative">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={referrerSearchQuery}
|
||||||
|
onChange={(e) => setReferrerSearchQuery(e.target.value)}
|
||||||
|
placeholder={t('admin.users.detail.referrals.searchPlaceholder')}
|
||||||
|
className="flex-1 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2.5 text-sm text-dark-100 placeholder-dark-500 focus:border-accent-500 focus:outline-none"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowReferrerSearch(false);
|
||||||
|
setReferrerSearchQuery('');
|
||||||
|
setReferrerSearchResults([]);
|
||||||
|
}}
|
||||||
|
className="rounded-lg bg-dark-700 px-3 py-2.5 text-sm text-dark-400 hover:bg-dark-600"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{referrerSearchQuery.length >= 2 && referrerSearchResults.length > 0 && (
|
||||||
|
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-y-auto rounded-xl border border-dark-700 bg-dark-800 py-1 shadow-xl">
|
||||||
|
{referrerSearchResults
|
||||||
|
.filter((u) => u.id !== userId)
|
||||||
|
.map((u) => (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
onClick={() => handleAssignReferrer(u.id)}
|
||||||
|
className="flex w-full items-center gap-3 px-3 py-2.5 text-left hover:bg-dark-700/50"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-dark-600/50 text-xs font-bold text-dark-300">
|
||||||
|
{(u.full_name || u.username || '?')[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-dark-100">
|
||||||
|
{u.full_name || u.username || `ID: ${u.id}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{u.telegram_id ? `TG: ${u.telegram_id}` : `ID: ${u.id}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{referrerSearchResults.filter((u) => u.id !== userId).length === 0 && (
|
||||||
|
<div className="px-3 py-4 text-center text-sm text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.noUsersFound')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{referrerSearchQuery.length >= 2 && referrerSearchResults.length === 0 && (
|
||||||
|
<div className="absolute left-0 right-0 top-full z-50 mt-1 rounded-xl border border-dark-700 bg-dark-800 py-4 text-center text-sm text-dark-500 shadow-xl">
|
||||||
|
{t('admin.users.detail.referrals.noUsersFound')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.noReferrer')}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReferrerSearch(true)}
|
||||||
|
className="rounded-lg bg-accent-500/15 px-3 py-2 text-sm text-accent-400 transition-colors hover:bg-accent-500/25"
|
||||||
|
>
|
||||||
|
{t('admin.users.detail.referrals.assignReferrer')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 2: Referral stats */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
|
<div className="rounded-xl bg-dark-800/40 p-4">
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.totalReferrals')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xl font-bold text-dark-100">
|
||||||
|
{user.referral.referrals_count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-dark-800/40 p-4">
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.totalEarnings')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xl font-bold text-dark-100">
|
||||||
|
{formatWithCurrency(user.referral.total_earnings_kopeks / 100)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-dark-800/40 p-4">
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.commission')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xl font-bold text-dark-100">
|
||||||
|
{user.referral.commission_percent ?? t('admin.users.detail.referrals.default')}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-dark-800/40 p-4">
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.referralCode')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 truncate font-mono text-sm text-dark-100">
|
||||||
|
{user.referral.referral_code}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 3: Referrals list */}
|
||||||
|
<div className="rounded-2xl border border-dark-700/30 bg-dark-800/40 p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-base font-semibold text-dark-100">
|
||||||
|
{t('admin.users.detail.referrals.referralsList')} ({referralsTotal})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{referralsListLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
) : referralsList.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-dark-500">
|
||||||
|
{t('admin.users.detail.referrals.noReferrals')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{referralsList.map((ref) => (
|
||||||
|
<div
|
||||||
|
key={ref.id}
|
||||||
|
className="flex items-center justify-between rounded-xl bg-dark-700/20 px-4 py-3"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/admin/users/${ref.id}`)}
|
||||||
|
className="flex items-center gap-3 text-left"
|
||||||
|
>
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-dark-600/50 text-sm font-bold text-dark-300">
|
||||||
|
{(ref.full_name || ref.username || '?')[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-dark-100">
|
||||||
|
{ref.full_name || ref.username || `ID: ${ref.id}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-dark-500">
|
||||||
|
{ref.telegram_id ? `TG: ${ref.telegram_id}` : `ID: ${ref.id}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveReferral(ref.id)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="rounded-lg p-2 text-dark-500 transition-colors hover:bg-error-500/10 hover:text-error-400 disabled:opacity-50"
|
||||||
|
title={t('admin.users.detail.referrals.removeReferral')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user