fix: show all campaigns in assign list, add dual links and bonus details

1. Remove is_active filter from campaign assign page so inactive
   campaigns are also visible (with badge)
2. Redesign partner campaign cards with separate bot/cabinet links,
   detailed bonus descriptions, and per-link copy buttons
This commit is contained in:
Fringg
2026-02-17 10:54:36 +03:00
parent 8b33d8224d
commit a72042d807
6 changed files with 138 additions and 70 deletions

View File

@@ -671,10 +671,18 @@
"submitApplication": "Submit Application", "submitApplication": "Submit Application",
"applyError": "Failed to submit application. Please try again.", "applyError": "Failed to submit application. Please try again.",
"yourCampaigns": "Your campaigns", "yourCampaigns": "Your campaigns",
"campaignLinks": {
"bot": "Bot link",
"web": "Cabinet link"
},
"campaignBonus": { "campaignBonus": {
"title": "Referral bonus",
"balance": "+{{amount}} to balance", "balance": "+{{amount}} to balance",
"balanceDesc": "{{amount}} credited on signup",
"subscription": "{{days}} days subscription", "subscription": "{{days}} days subscription",
"tariff": "Bonus tariff" "subscriptionDesc": "{{days}} days free subscription",
"tariff": "Bonus tariff",
"tariffDesc": "Free tariff plan"
}, },
"fields": { "fields": {
"companyName": "Company Name", "companyName": "Company Name",

View File

@@ -562,10 +562,18 @@
"submitApplication": "ارسال درخواست", "submitApplication": "ارسال درخواست",
"applyError": "ارسال درخواست ناموفق بود. لطفاً دوباره تلاش کنید.", "applyError": "ارسال درخواست ناموفق بود. لطفاً دوباره تلاش کنید.",
"yourCampaigns": "کمپین‌های شما", "yourCampaigns": "کمپین‌های شما",
"campaignLinks": {
"bot": "لینک ربات",
"web": "لینک کابینت"
},
"campaignBonus": { "campaignBonus": {
"title": "جایزه معرفی",
"balance": "+{{amount}} به موجودی", "balance": "+{{amount}} به موجودی",
"balanceDesc": "{{amount}} هنگام ثبت‌نام",
"subscription": "{{days}} روز اشتراک", "subscription": "{{days}} روز اشتراک",
"tariff": "تعرفه جایزه" "subscriptionDesc": "{{days}} روز اشتراک رایگان",
"tariff": "تعرفه جایزه",
"tariffDesc": "طرح تعرفه رایگان"
}, },
"fields": { "fields": {
"companyName": "نام شرکت", "companyName": "نام شرکت",

View File

@@ -698,10 +698,18 @@
"submitApplication": "Отправить заявку", "submitApplication": "Отправить заявку",
"applyError": "Не удалось отправить заявку. Попробуйте снова.", "applyError": "Не удалось отправить заявку. Попробуйте снова.",
"yourCampaigns": "Ваши кампании", "yourCampaigns": "Ваши кампании",
"campaignLinks": {
"bot": "Ссылка в бот",
"web": "Ссылка в кабинет"
},
"campaignBonus": { "campaignBonus": {
"title": "Бонус для реферала",
"balance": "+{{amount}} на баланс", "balance": "+{{amount}} на баланс",
"balanceDesc": "{{amount}} на баланс при регистрации",
"subscription": "{{days}} дн. подписки", "subscription": "{{days}} дн. подписки",
"tariff": "Бонусный тариф" "subscriptionDesc": "{{days}} дней бесплатной подписки",
"tariff": "Бонусный тариф",
"tariffDesc": "Бесплатный тарифный план"
}, },
"fields": { "fields": {
"companyName": "Название компании", "companyName": "Название компании",

View File

@@ -562,10 +562,18 @@
"submitApplication": "提交申请", "submitApplication": "提交申请",
"applyError": "提交申请失败,请重试。", "applyError": "提交申请失败,请重试。",
"yourCampaigns": "您的活动", "yourCampaigns": "您的活动",
"campaignLinks": {
"bot": "机器人链接",
"web": "网页链接"
},
"campaignBonus": { "campaignBonus": {
"title": "推荐奖励",
"balance": "+{{amount}} 余额", "balance": "+{{amount}} 余额",
"balanceDesc": "注册时充值 {{amount}}",
"subscription": "{{days}} 天订阅", "subscription": "{{days}} 天订阅",
"tariff": "奖励套餐" "subscriptionDesc": "{{days}} 天免费订阅",
"tariff": "奖励套餐",
"tariffDesc": "免费套餐方案"
}, },
"fields": { "fields": {
"companyName": "公司名称", "companyName": "公司名称",

View File

@@ -33,8 +33,7 @@ export default function AdminPartnerCampaignAssign() {
}); });
const assignedIds = new Set(partner?.campaigns.map((c) => c.id) ?? []); const assignedIds = new Set(partner?.campaigns.map((c) => c.id) ?? []);
const available = const available = campaignsData?.campaigns.filter((c) => !assignedIds.has(c.id)) ?? [];
campaignsData?.campaigns.filter((c) => !assignedIds.has(c.id) && c.is_active) ?? [];
const partnerName = partner const partnerName = partner
? partner.first_name || partner.username || `#${partner.user_id}` ? partner.first_name || partner.username || `#${partner.user_id}`
@@ -75,7 +74,14 @@ export default function AdminPartnerCampaignAssign() {
<div key={campaign.id} className="rounded-xl border border-dark-700 bg-dark-800 p-4"> <div key={campaign.id} className="rounded-xl border border-dark-700 bg-dark-800 p-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-medium text-dark-100">{campaign.name}</div> <div className="flex items-center gap-2">
<span className="font-medium text-dark-100">{campaign.name}</span>
{!campaign.is_active && (
<span className="rounded bg-dark-600 px-1.5 py-0.5 text-xs text-dark-400">
{t('admin.campaigns.status.inactive')}
</span>
)}
</div>
<div className="mt-1 flex items-center gap-3 text-xs text-dark-500"> <div className="mt-1 flex items-center gap-3 text-xs text-dark-500">
<span className="font-mono">?start={campaign.start_parameter}</span> <span className="font-mono">?start={campaign.start_parameter}</span>
<span> <span>

View File

@@ -93,7 +93,7 @@ export default function Referral() {
const { formatAmount, currencySymbol, formatPositive, formatWithCurrency } = useCurrency(); const { formatAmount, currencySymbol, formatPositive, formatWithCurrency } = useCurrency();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [copiedCampaignId, setCopiedCampaignId] = useState<number | null>(null); const [copiedLink, setCopiedLink] = useState<string | null>(null);
const { data: info, isLoading } = useQuery({ const { data: info, isLoading } = useQuery({
queryKey: ['referral-info'], queryKey: ['referral-info'],
@@ -522,8 +522,8 @@ export default function Referral() {
{/* ==================== Partner Campaigns Section ==================== */} {/* ==================== Partner Campaigns Section ==================== */}
{isPartner && partnerStatus?.campaigns && partnerStatus.campaigns.length > 0 && ( {isPartner && partnerStatus?.campaigns && partnerStatus.campaigns.length > 0 && (
<div className="bento-card"> <div className="space-y-4">
<div className="mb-4 flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-500/10 text-accent-400"> <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-500/10 text-accent-400">
<LinkIcon /> <LinkIcon />
</div> </div>
@@ -532,70 +532,100 @@ export default function Referral() {
</h2> </h2>
</div> </div>
<div className="space-y-3">
{partnerStatus.campaigns.map((campaign) => { {partnerStatus.campaigns.map((campaign) => {
const shareUrl = campaign.deep_link || campaign.web_link || ''; const copyLink = (url: string, key: string) => {
const isCopied = copiedCampaignId === campaign.id; navigator.clipboard.writeText(url);
setCopiedLink(key);
const copyCampaignLink = () => { setTimeout(() => setCopiedLink(null), 2000);
if (!shareUrl) return;
navigator.clipboard.writeText(shareUrl);
setCopiedCampaignId(campaign.id);
setTimeout(() => setCopiedCampaignId(null), 2000);
}; };
const bonusBadge = (() => { const botKey = `${campaign.id}-bot`;
switch (campaign.bonus_type) { const webKey = `${campaign.id}-web`;
case 'balance':
return {
label: t('referral.partner.campaignBonus.balance', {
amount: formatWithCurrency(campaign.balance_bonus_kopeks / 100, 0),
}),
className: 'badge-success',
};
case 'subscription':
return {
label: t('referral.partner.campaignBonus.subscription', {
days: campaign.subscription_duration_days ?? 0,
}),
className: 'badge-info',
};
case 'tariff':
return {
label: t('referral.partner.campaignBonus.tariff'),
className: 'badge-info',
};
default:
return null;
}
})();
return ( return (
<div <div key={campaign.id} className="bento-card space-y-4">
key={campaign.id} {/* Campaign header */}
className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-4" <div className="flex items-center gap-2">
> <h3 className="text-base font-semibold text-dark-100">{campaign.name}</h3>
<div className="mb-2 flex items-center gap-2"> </div>
<span className="font-medium text-dark-100">{campaign.name}</span>
{bonusBadge && <span className={bonusBadge.className}>{bonusBadge.label}</span>} {/* Bonus info */}
{campaign.bonus_type !== 'none' && (
<div className="rounded-lg bg-success-500/10 p-3">
<div className="mb-1 text-xs font-medium text-success-500">
{t('referral.partner.campaignBonus.title')}
</div>
<div className="text-sm font-semibold text-success-400">
{campaign.bonus_type === 'balance' &&
t('referral.partner.campaignBonus.balanceDesc', {
amount: formatWithCurrency(campaign.balance_bonus_kopeks / 100, 0),
})}
{campaign.bonus_type === 'subscription' &&
t('referral.partner.campaignBonus.subscriptionDesc', {
days: campaign.subscription_duration_days ?? 0,
...(campaign.subscription_traffic_gb
? { traffic: campaign.subscription_traffic_gb }
: {}),
})}
{campaign.bonus_type === 'tariff' &&
t('referral.partner.campaignBonus.tariffDesc')}
</div>
</div>
)}
{/* Bot link */}
{campaign.deep_link && (
<div>
<div className="mb-1 text-xs font-medium text-dark-500">
{t('referral.partner.campaignLinks.bot')}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input type="text" readOnly value={shareUrl} className="input flex-1 text-xs" /> <input
type="text"
readOnly
value={campaign.deep_link}
className="input flex-1 text-xs"
/>
<button <button
onClick={copyCampaignLink} onClick={() => copyLink(campaign.deep_link!, botKey)}
disabled={!shareUrl}
className={`btn-primary shrink-0 px-3 py-2 ${ className={`btn-primary shrink-0 px-3 py-2 ${
isCopied ? 'bg-success-500 hover:bg-success-500' : '' copiedLink === botKey ? 'bg-success-500 hover:bg-success-500' : ''
}`} }`}
> >
{isCopied ? <CheckIcon /> : <CopyIcon />} {copiedLink === botKey ? <CheckIcon /> : <CopyIcon />}
</button> </button>
</div> </div>
</div> </div>
)}
{/* Web link */}
{campaign.web_link && (
<div>
<div className="mb-1 text-xs font-medium text-dark-500">
{t('referral.partner.campaignLinks.web')}
</div>
<div className="flex items-center gap-2">
<input
type="text"
readOnly
value={campaign.web_link}
className="input flex-1 text-xs"
/>
<button
onClick={() => copyLink(campaign.web_link!, webKey)}
className={`btn-primary shrink-0 px-3 py-2 ${
copiedLink === webKey ? 'bg-success-500 hover:bg-success-500' : ''
}`}
>
{copiedLink === webKey ? <CheckIcon /> : <CopyIcon />}
</button>
</div>
</div>
)}
</div>
); );
})} })}
</div> </div>
</div>
)} )}
{/* ==================== Withdrawal Section (approved partners only) ==================== */} {/* ==================== Withdrawal Section (approved partners only) ==================== */}