feat: add inline referral commission editing in admin user card

Admins can now edit individual referral commission percent directly
in the user detail card. Shows "Default" when no custom value is set.
This commit is contained in:
Fringg
2026-02-08 20:29:56 +03:00
parent 23f56afaf7
commit 92d206f5b6
6 changed files with 93 additions and 12 deletions

View File

@@ -447,6 +447,17 @@ export const adminUsersApi = {
return response.data;
},
// Update referral commission
updateReferralCommission: async (
userId: number,
commissionPercent: number | null,
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/referral-commission`, {
commission_percent: commissionPercent,
});
return response.data;
},
// Delete user (soft delete, does NOT remove from Remnawave)
deleteUser: async (
userId: number,

View File

@@ -1913,7 +1913,9 @@
"title": "Referral program",
"referrals": "Referrals",
"earned": "Earned",
"commission": "Commission"
"commission": "Commission",
"default": "Default",
"invalidPercent": "Commission must be between 0 and 100"
},
"restrictions": {
"title": "Restrictions",

View File

@@ -1616,7 +1616,9 @@
"title": "برنامه ارجاع",
"referrals": "ارجاعات",
"earned": "درآمد",
"commission": "کمیسیون"
"commission": "کمیسیون",
"default": "پیش‌فرض",
"invalidPercent": "کمیسیون باید بین ۰ تا ۱۰۰ باشد"
},
"restrictions": {
"title": "محدودیت‌ها",

View File

@@ -2439,7 +2439,9 @@
"title": "Реферальная программа",
"referrals": "Рефералов",
"earned": "Заработано",
"commission": "Комиссия"
"commission": "Комиссия",
"default": "По умолч.",
"invalidPercent": "Процент должен быть от 0 до 100"
},
"restrictions": {
"title": "Ограничения",

View File

@@ -1615,7 +1615,9 @@
"title": "推荐计划",
"referrals": "推荐人数",
"earned": "已赚取",
"commission": "佣金"
"commission": "佣金",
"default": "默认",
"invalidPercent": "佣金必须在0到100之间"
},
"restrictions": {
"title": "限制",

View File

@@ -186,6 +186,10 @@ export default function AdminUserDetail() {
const [promoGroups, setPromoGroups] = useState<PromoGroup[]>([]);
const [editingPromoGroup, setEditingPromoGroup] = useState(false);
// Referral commission
const [editingReferralCommission, setEditingReferralCommission] = useState(false);
const [referralCommissionValue, setReferralCommissionValue] = useState<number | ''>('');
// Send promo offer
const [offerDiscountPercent, setOfferDiscountPercent] = useState<number | ''>('');
const [offerValidHours, setOfferValidHours] = useState<number | ''>(24);
@@ -503,6 +507,25 @@ export default function AdminUserDetail() {
}
};
const handleUpdateReferralCommission = async () => {
if (!userId) return;
setActionLoading(true);
try {
const value = referralCommissionValue === '' ? null : toNumber(referralCommissionValue);
if (value !== null && (value < 0 || value > 100)) {
notify.error(t('admin.users.detail.referral.invalidPercent'), t('common.error'));
return;
}
await adminUsersApi.updateReferralCommission(userId, value);
await loadUser();
setEditingReferralCommission(false);
} catch {
notify.error(t('admin.users.userActions.error'), t('common.error'));
} finally {
setActionLoading(false);
}
};
const handleDeactivateOffer = async () => {
if (!userId) return;
setActionLoading(true);
@@ -850,8 +873,21 @@ export default function AdminUserDetail() {
{/* Referral */}
<div className="rounded-xl bg-dark-800/50 p-3">
<div className="mb-2 text-sm font-medium text-dark-200">
{t('admin.users.detail.referral.title')}
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-dark-200">
{t('admin.users.detail.referral.title')}
</span>
<button
onClick={() => {
if (!editingReferralCommission) {
setReferralCommissionValue(user.referral.commission_percent ?? '');
}
setEditingReferralCommission(!editingReferralCommission);
}}
className="text-xs text-accent-400 transition-colors hover:text-accent-300"
>
{editingReferralCommission ? t('common.cancel') : t('common.edit')}
</button>
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
@@ -871,12 +907,38 @@ export default function AdminUserDetail() {
</div>
</div>
<div>
<div className="text-lg font-bold text-dark-100">
{user.referral.commission_percent || 0}%
</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.referral.commission')}
</div>
{editingReferralCommission ? (
<div className="space-y-1">
<input
type="number"
value={referralCommissionValue}
onChange={createNumberInputHandler(setReferralCommissionValue, 0)}
placeholder="0-100"
className="input w-full text-center text-sm"
min={0}
max={100}
disabled={actionLoading}
/>
<button
onClick={handleUpdateReferralCommission}
disabled={actionLoading}
className="w-full rounded-lg bg-accent-500 px-2 py-1 text-xs text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
>
{actionLoading ? t('common.loading') : t('common.save')}
</button>
</div>
) : (
<>
<div className="text-lg font-bold text-dark-100">
{user.referral.commission_percent != null
? `${user.referral.commission_percent}%`
: t('admin.users.detail.referral.default')}
</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.referral.commission')}
</div>
</>
)}
</div>
</div>
</div>