mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add "Add Referral" button to referrals tab
- New search UI in referrals list section to find and bind users as this user's referrals (calls assign-referrer on target user) - Debounced search with cancelled flag and loading state - Click-outside to close dropdown - i18n: addReferral, referralAdded keys in all 4 locales
This commit is contained in:
@@ -3118,6 +3118,8 @@
|
|||||||
"noReferrals": "No referrals yet",
|
"noReferrals": "No referrals yet",
|
||||||
"removeReferral": "Remove referral",
|
"removeReferral": "Remove referral",
|
||||||
"referralRemoved": "Referral removed successfully",
|
"referralRemoved": "Referral removed successfully",
|
||||||
|
"addReferral": "Add Referral",
|
||||||
|
"referralAdded": "Referral added successfully",
|
||||||
"searchPlaceholder": "Search by name, email, or Telegram ID...",
|
"searchPlaceholder": "Search by name, email, or Telegram ID...",
|
||||||
"alreadyReferred": "already has referrer",
|
"alreadyReferred": "already has referrer",
|
||||||
"noUsersFound": "No users found"
|
"noUsersFound": "No users found"
|
||||||
|
|||||||
@@ -2736,6 +2736,8 @@
|
|||||||
"noReferrals": "هنوز معرفیای نیست",
|
"noReferrals": "هنوز معرفیای نیست",
|
||||||
"removeReferral": "حذف معرفی",
|
"removeReferral": "حذف معرفی",
|
||||||
"referralRemoved": "معرفی حذف شد",
|
"referralRemoved": "معرفی حذف شد",
|
||||||
|
"addReferral": "افزودن معرفی",
|
||||||
|
"referralAdded": "معرفی اضافه شد",
|
||||||
"searchPlaceholder": "جستجو بر اساس نام، ایمیل یا شناسه تلگرام...",
|
"searchPlaceholder": "جستجو بر اساس نام، ایمیل یا شناسه تلگرام...",
|
||||||
"alreadyReferred": "قبلاً معرفی شده",
|
"alreadyReferred": "قبلاً معرفی شده",
|
||||||
"noUsersFound": "کاربری یافت نشد"
|
"noUsersFound": "کاربری یافت نشد"
|
||||||
|
|||||||
@@ -3629,6 +3629,8 @@
|
|||||||
"noReferrals": "Рефералов пока нет",
|
"noReferrals": "Рефералов пока нет",
|
||||||
"removeReferral": "Удалить реферала",
|
"removeReferral": "Удалить реферала",
|
||||||
"referralRemoved": "Реферал удалён",
|
"referralRemoved": "Реферал удалён",
|
||||||
|
"addReferral": "Добавить реферала",
|
||||||
|
"referralAdded": "Реферал добавлен",
|
||||||
"searchPlaceholder": "Поиск по имени, email или Telegram ID...",
|
"searchPlaceholder": "Поиск по имени, email или Telegram ID...",
|
||||||
"alreadyReferred": "уже чей-то реферал",
|
"alreadyReferred": "уже чей-то реферал",
|
||||||
"noUsersFound": "Пользователи не найдены"
|
"noUsersFound": "Пользователи не найдены"
|
||||||
|
|||||||
@@ -2735,6 +2735,8 @@
|
|||||||
"noReferrals": "暂无推荐",
|
"noReferrals": "暂无推荐",
|
||||||
"removeReferral": "移除推荐",
|
"removeReferral": "移除推荐",
|
||||||
"referralRemoved": "推荐已移除",
|
"referralRemoved": "推荐已移除",
|
||||||
|
"addReferral": "添加推荐",
|
||||||
|
"referralAdded": "推荐已添加",
|
||||||
"searchPlaceholder": "按名称、邮箱或 Telegram ID 搜索...",
|
"searchPlaceholder": "按名称、邮箱或 Telegram ID 搜索...",
|
||||||
"alreadyReferred": "已有推荐人",
|
"alreadyReferred": "已有推荐人",
|
||||||
"noUsersFound": "未找到用户"
|
"noUsersFound": "未找到用户"
|
||||||
|
|||||||
@@ -315,6 +315,12 @@ export default function AdminUserDetail() {
|
|||||||
const [referrerSearchResults, setReferrerSearchResults] = useState<UserListItem[]>([]);
|
const [referrerSearchResults, setReferrerSearchResults] = useState<UserListItem[]>([]);
|
||||||
const [referrerSearchLoading, setReferrerSearchLoading] = useState(false);
|
const [referrerSearchLoading, setReferrerSearchLoading] = useState(false);
|
||||||
const referrerSearchRef = useRef<HTMLDivElement>(null);
|
const referrerSearchRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Add referral (bind someone as this user's referral)
|
||||||
|
const [showAddReferral, setShowAddReferral] = useState(false);
|
||||||
|
const [addReferralSearchQuery, setAddReferralSearchQuery] = useState('');
|
||||||
|
const [addReferralSearchResults, setAddReferralSearchResults] = useState<UserListItem[]>([]);
|
||||||
|
const [addReferralSearchLoading, setAddReferralSearchLoading] = useState(false);
|
||||||
|
const addReferralSearchRef = 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);
|
||||||
@@ -950,6 +956,26 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a referral: assign current user as referrer of selected user
|
||||||
|
const handleAddReferral = async (targetUserId: number) => {
|
||||||
|
if (!userId) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await adminUsersApi.assignReferrer(targetUserId, userId);
|
||||||
|
await loadReferralsList();
|
||||||
|
await loadUser();
|
||||||
|
setShowAddReferral(false);
|
||||||
|
setAddReferralSearchQuery('');
|
||||||
|
setAddReferralSearchResults([]);
|
||||||
|
notify.success(t('admin.users.detail.referrals.referralAdded'));
|
||||||
|
} 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
|
// Referrals tab: debounced user search for referrer assignment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (referrerSearchQuery.length < 2 || !showReferrerSearch) {
|
if (referrerSearchQuery.length < 2 || !showReferrerSearch) {
|
||||||
@@ -994,6 +1020,53 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
}, [showReferrerSearch]);
|
}, [showReferrerSearch]);
|
||||||
|
|
||||||
|
// Referrals tab: debounced search for adding referral
|
||||||
|
useEffect(() => {
|
||||||
|
if (addReferralSearchQuery.length < 2 || !showAddReferral) {
|
||||||
|
setAddReferralSearchResults([]);
|
||||||
|
setAddReferralSearchLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAddReferralSearchLoading(true);
|
||||||
|
let cancelled = false;
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const data = await adminUsersApi.getUsers({ search: addReferralSearchQuery, limit: 10 });
|
||||||
|
if (!cancelled) {
|
||||||
|
setAddReferralSearchResults(data.users || []);
|
||||||
|
setAddReferralSearchLoading(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setAddReferralSearchResults([]);
|
||||||
|
setAddReferralSearchLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [addReferralSearchQuery, showAddReferral]);
|
||||||
|
|
||||||
|
// Referrals tab: close add-referral dropdown on click outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
addReferralSearchRef.current &&
|
||||||
|
!addReferralSearchRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setShowAddReferral(false);
|
||||||
|
setAddReferralSearchQuery('');
|
||||||
|
setAddReferralSearchResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (showAddReferral) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [showAddReferral]);
|
||||||
|
|
||||||
const handleResetTrial = async () => {
|
const handleResetTrial = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
@@ -3146,8 +3219,80 @@ export default function AdminUserDetail() {
|
|||||||
<h3 className="text-base font-semibold text-dark-100">
|
<h3 className="text-base font-semibold text-dark-100">
|
||||||
{t('admin.users.detail.referrals.referralsList')} ({referralsTotal})
|
{t('admin.users.detail.referrals.referralsList')} ({referralsTotal})
|
||||||
</h3>
|
</h3>
|
||||||
|
{!showAddReferral && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddReferral(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.addReferral')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Add referral search */}
|
||||||
|
{showAddReferral && (
|
||||||
|
<div ref={addReferralSearchRef} className="relative mb-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={addReferralSearchQuery}
|
||||||
|
onChange={(e) => setAddReferralSearchQuery(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={() => {
|
||||||
|
setShowAddReferral(false);
|
||||||
|
setAddReferralSearchQuery('');
|
||||||
|
setAddReferralSearchResults([]);
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
{addReferralSearchQuery.length >= 2 && addReferralSearchResults.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">
|
||||||
|
{addReferralSearchResults
|
||||||
|
.filter((u) => u.id !== userId)
|
||||||
|
.map((u) => (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
onClick={() => handleAddReferral(u.id)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="flex w-full items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-dark-700/50 disabled:opacity-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>
|
||||||
|
))}
|
||||||
|
{addReferralSearchResults.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>
|
||||||
|
)}
|
||||||
|
{addReferralSearchQuery.length >= 2 &&
|
||||||
|
!addReferralSearchLoading &&
|
||||||
|
addReferralSearchResults.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>
|
||||||
|
)}
|
||||||
|
|
||||||
{referralsListLoading ? (
|
{referralsListLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<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 className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||||
|
|||||||
Reference in New Issue
Block a user