mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: partner-campaign integration in admin UI
- Add partner badge to campaigns list - Add partner selector dropdown in campaign edit - Support ?partnerId= query param in campaign create (replaces separate page) - Filter already-assigned campaigns in partner assign flow - Show taken campaigns greyed out with partner name - Delete AdminPartnerCampaignCreate (replaced by query param approach) - Add partner translations (ru, en, zh, fa)
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -66,7 +66,6 @@ const AdminApplicationReview = lazy(() => import('./pages/AdminApplicationReview
|
||||
const AdminPartnerCommission = lazy(() => import('./pages/AdminPartnerCommission'));
|
||||
const AdminPartnerRevoke = lazy(() => import('./pages/AdminPartnerRevoke'));
|
||||
const AdminPartnerCampaignAssign = lazy(() => import('./pages/AdminPartnerCampaignAssign'));
|
||||
const AdminPartnerCampaignCreate = lazy(() => import('./pages/AdminPartnerCampaignCreate'));
|
||||
const AdminWithdrawals = lazy(() => import('./pages/AdminWithdrawals'));
|
||||
const AdminWithdrawalDetail = lazy(() => import('./pages/AdminWithdrawalDetail'));
|
||||
const AdminWithdrawalReject = lazy(() => import('./pages/AdminWithdrawalReject'));
|
||||
@@ -642,16 +641,6 @@ function App() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/partners/:userId/campaigns/create"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<LazyPage>
|
||||
<AdminPartnerCampaignCreate />
|
||||
</LazyPage>
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/partners/:userId"
|
||||
element={
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface CampaignListItem {
|
||||
registrations_count: number;
|
||||
total_revenue_kopeks: number;
|
||||
conversion_rate: number;
|
||||
partner_user_id: number | null;
|
||||
partner_name: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,8 @@ export interface CampaignDetail {
|
||||
tariff_id: number | null;
|
||||
tariff_duration_days: number | null;
|
||||
tariff: TariffInfo | null;
|
||||
partner_user_id: number | null;
|
||||
partner_name: string | null;
|
||||
created_by: number | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
@@ -59,6 +63,7 @@ export interface CampaignCreateRequest {
|
||||
subscription_squads?: string[];
|
||||
tariff_id?: number;
|
||||
tariff_duration_days?: number;
|
||||
partner_user_id?: number | null;
|
||||
}
|
||||
|
||||
export interface CampaignUpdateRequest {
|
||||
@@ -73,6 +78,7 @@ export interface CampaignUpdateRequest {
|
||||
subscription_squads?: string[];
|
||||
tariff_id?: number;
|
||||
tariff_duration_days?: number;
|
||||
partner_user_id?: number | null;
|
||||
}
|
||||
|
||||
export interface CampaignToggleResponse {
|
||||
@@ -159,6 +165,12 @@ export interface TariffListItem {
|
||||
device_limit: number;
|
||||
}
|
||||
|
||||
export interface AvailablePartner {
|
||||
user_id: number;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
}
|
||||
|
||||
export const campaignsApi = {
|
||||
// Get campaigns overview
|
||||
getOverview: async (): Promise<CampaignsOverview> => {
|
||||
@@ -240,4 +252,10 @@ export const campaignsApi = {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available partners for campaign assignment
|
||||
getAvailablePartners: async (): Promise<AvailablePartner[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/campaigns/available-partners');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1747,7 +1747,11 @@
|
||||
"active": "Active",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"saving": "Saving..."
|
||||
"saving": "Saving...",
|
||||
"partner": "Partner",
|
||||
"noPartner": "No partner",
|
||||
"selectPartner": "Select partner",
|
||||
"partnerAutoAssign": "Campaign will be automatically assigned to partner {{name}}"
|
||||
},
|
||||
"stats": {
|
||||
"title": "Campaign Statistics",
|
||||
|
||||
@@ -1486,7 +1486,11 @@
|
||||
"active": "فعال",
|
||||
"cancel": "لغو",
|
||||
"save": "ذخیره",
|
||||
"saving": "در حال ذخیره..."
|
||||
"saving": "در حال ذخیره...",
|
||||
"partner": "شریک",
|
||||
"noPartner": "بدون شریک",
|
||||
"selectPartner": "انتخاب شریک",
|
||||
"partnerAutoAssign": "کمپین به طور خودکار به شریک {{name}} اختصاص داده میشود"
|
||||
},
|
||||
"stats": {
|
||||
"title": "آمار کمپین",
|
||||
|
||||
@@ -2267,7 +2267,11 @@
|
||||
"active": "Активна",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение..."
|
||||
"saving": "Сохранение...",
|
||||
"partner": "Партнёр",
|
||||
"noPartner": "Без партнёра",
|
||||
"selectPartner": "Выберите партнёра",
|
||||
"partnerAutoAssign": "Кампания будет автоматически привязана к партнёру {{name}}"
|
||||
},
|
||||
"stats": {
|
||||
"title": "Статистика кампании",
|
||||
|
||||
@@ -1485,7 +1485,11 @@
|
||||
"active": "启用",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"saving": "保存中..."
|
||||
"saving": "保存中...",
|
||||
"partner": "合作伙伴",
|
||||
"noPartner": "无合作伙伴",
|
||||
"selectPartner": "选择合作伙伴",
|
||||
"partnerAutoAssign": "活动将自动分配给合作伙伴 {{name}}"
|
||||
},
|
||||
"stats": {
|
||||
"title": "活动统计",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ServerSquadInfo,
|
||||
TariffListItem,
|
||||
} from '../api/campaigns';
|
||||
import { partnerApi } from '../api/partners';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||
// Icons
|
||||
@@ -149,10 +150,27 @@ function TariffSelector({
|
||||
);
|
||||
}
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/gi, '_')
|
||||
.replace(/^_|_$/g, '')
|
||||
.slice(0, 30);
|
||||
}
|
||||
|
||||
export default function AdminCampaignCreate() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
const partnerId = searchParams.get('partnerId');
|
||||
|
||||
// Fetch partner info if creating for a partner
|
||||
const { data: partner } = useQuery({
|
||||
queryKey: ['admin-partner-detail', partnerId],
|
||||
queryFn: () => partnerApi.getPartnerDetail(Number(partnerId)),
|
||||
enabled: !!partnerId,
|
||||
});
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
@@ -160,6 +178,17 @@ export default function AdminCampaignCreate() {
|
||||
const [bonusType, setBonusType] = useState<CampaignBonusType>('balance');
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
|
||||
// Auto-fill from partner
|
||||
const [autoFilled, setAutoFilled] = useState(false);
|
||||
useEffect(() => {
|
||||
if (partner && !autoFilled) {
|
||||
const partnerName = partner.first_name || partner.username || '';
|
||||
if (!name && partnerName) setName(partnerName);
|
||||
if (!startParameter && partnerName) setStartParameter(`partner_${slugify(partnerName)}`);
|
||||
setAutoFilled(true);
|
||||
}
|
||||
}, [partner, autoFilled, name, startParameter]);
|
||||
|
||||
// Balance bonus
|
||||
const [balanceBonusRubles, setBalanceBonusRubles] = useState<number | ''>(0);
|
||||
|
||||
@@ -191,7 +220,12 @@ export default function AdminCampaignCreate() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-campaigns-overview'] });
|
||||
navigate('/admin/campaigns');
|
||||
if (partnerId) {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-partner-detail', partnerId] });
|
||||
navigate(`/admin/partners/${partnerId}`);
|
||||
} else {
|
||||
navigate('/admin/campaigns');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -221,6 +255,10 @@ export default function AdminCampaignCreate() {
|
||||
data.tariff_duration_days = toNumber(tariffDays, 30);
|
||||
}
|
||||
|
||||
if (partnerId) {
|
||||
data.partner_user_id = Number(partnerId);
|
||||
}
|
||||
|
||||
createMutation.mutate(data);
|
||||
};
|
||||
|
||||
@@ -247,6 +285,32 @@ export default function AdminCampaignCreate() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Partner info banner */}
|
||||
{partnerId && partner && (
|
||||
<div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
className="mt-0.5 h-5 w-5 shrink-0 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-accent-300">
|
||||
{t('admin.campaigns.form.partnerAutoAssign', {
|
||||
name: partner.first_name || partner.username || `#${partnerId}`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Info */}
|
||||
<div className="card space-y-4">
|
||||
{/* Name */}
|
||||
@@ -442,7 +506,10 @@ export default function AdminCampaignCreate() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="card flex items-center justify-end gap-3">
|
||||
<button onClick={() => navigate('/admin/campaigns')} className="btn-secondary">
|
||||
<button
|
||||
onClick={() => navigate(partnerId ? `/admin/partners/${partnerId}` : '/admin/campaigns')}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CampaignBonusType,
|
||||
ServerSquadInfo,
|
||||
TariffListItem,
|
||||
AvailablePartner,
|
||||
} from '../api/campaigns';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { CheckIcon, CampaignIcon } from '../components/icons';
|
||||
@@ -124,6 +125,40 @@ function TariffSelector({
|
||||
);
|
||||
}
|
||||
|
||||
// Partner selector component
|
||||
function PartnerSelector({
|
||||
partners,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
partners: AvailablePartner[];
|
||||
value: number | null;
|
||||
onChange: (id: number | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-dark-300">
|
||||
{t('admin.campaigns.form.partner')}
|
||||
</label>
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value ? parseInt(e.target.value) : null)}
|
||||
className="input"
|
||||
>
|
||||
<option value="">{t('admin.campaigns.form.noPartner')}</option>
|
||||
{partners.map((p) => (
|
||||
<option key={p.user_id} value={p.user_id}>
|
||||
{p.first_name || p.username || `#${p.user_id}`}
|
||||
{p.username ? ` (@${p.username})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminCampaignEdit() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -155,6 +190,12 @@ export default function AdminCampaignEdit() {
|
||||
queryFn: () => campaignsApi.getAvailableTariffs(),
|
||||
});
|
||||
|
||||
// Fetch partners
|
||||
const { data: partners = [] } = useQuery({
|
||||
queryKey: ['admin-campaigns-partners'],
|
||||
queryFn: () => campaignsApi.getAvailablePartners(),
|
||||
});
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
const [startParameter, setStartParameter] = useState('');
|
||||
@@ -174,6 +215,10 @@ export default function AdminCampaignEdit() {
|
||||
const [tariffId, setTariffId] = useState<number | null>(null);
|
||||
const [tariffDays, setTariffDays] = useState<number | ''>(30);
|
||||
|
||||
// Partner
|
||||
const [partnerUserId, setPartnerUserId] = useState<number | null>(null);
|
||||
const [initialPartnerUserId, setInitialPartnerUserId] = useState<number | null>(null);
|
||||
|
||||
// Initialize form when campaign loads
|
||||
useEffect(() => {
|
||||
if (campaign) {
|
||||
@@ -188,6 +233,8 @@ export default function AdminCampaignEdit() {
|
||||
setSelectedSquads(campaign.subscription_squads || []);
|
||||
setTariffId(campaign.tariff_id || null);
|
||||
setTariffDays(campaign.tariff_duration_days || 30);
|
||||
setPartnerUserId(campaign.partner_user_id ?? null);
|
||||
setInitialPartnerUserId(campaign.partner_user_id ?? null);
|
||||
}
|
||||
}, [campaign]);
|
||||
|
||||
@@ -215,6 +262,11 @@ export default function AdminCampaignEdit() {
|
||||
is_active: isActive,
|
||||
};
|
||||
|
||||
// Only send partner_user_id when it was actually changed
|
||||
if (partnerUserId !== initialPartnerUserId) {
|
||||
data.partner_user_id = partnerUserId;
|
||||
}
|
||||
|
||||
if (bonusType === 'balance') {
|
||||
data.balance_bonus_kopeks = Math.round(toNumber(balanceBonusRubles) * 100);
|
||||
} else if (bonusType === 'subscription') {
|
||||
@@ -344,6 +396,11 @@ export default function AdminCampaignEdit() {
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Partner */}
|
||||
{partners.length > 0 && (
|
||||
<PartnerSelector partners={partners} value={partnerUserId} onChange={setPartnerUserId} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bonus Type */}
|
||||
|
||||
@@ -183,6 +183,11 @@ export default function AdminCampaigns() {
|
||||
>
|
||||
{t(bonusTypeConfig[campaign.bonus_type].labelKey)}
|
||||
</span>
|
||||
{campaign.partner_name && (
|
||||
<span className="rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400">
|
||||
{campaign.partner_name}
|
||||
</span>
|
||||
)}
|
||||
{!campaign.is_active && (
|
||||
<span className="rounded bg-dark-600 px-2 py-0.5 text-xs text-dark-400">
|
||||
{t('admin.campaigns.table.inactive')}
|
||||
|
||||
@@ -33,7 +33,12 @@ export default function AdminPartnerCampaignAssign() {
|
||||
});
|
||||
|
||||
const assignedIds = new Set(partner?.campaigns.map((c) => c.id) ?? []);
|
||||
const available = campaignsData?.campaigns.filter((c) => !assignedIds.has(c.id)) ?? [];
|
||||
const available =
|
||||
campaignsData?.campaigns.filter((c) => !assignedIds.has(c.id) && c.partner_user_id === null) ??
|
||||
[];
|
||||
const takenByOthers =
|
||||
campaignsData?.campaigns.filter((c) => !assignedIds.has(c.id) && c.partner_user_id !== null) ??
|
||||
[];
|
||||
|
||||
const partnerName = partner
|
||||
? partner.first_name || partner.username || `#${partner.user_id}`
|
||||
@@ -62,7 +67,7 @@ export default function AdminPartnerCampaignAssign() {
|
||||
{t('admin.partnerDetail.campaigns.noAvailable')}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(`/admin/partners/${userId}/campaigns/create`)}
|
||||
onClick={() => navigate(`/admin/campaigns/create?partnerId=${userId}`)}
|
||||
className="mt-2 w-full rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600"
|
||||
>
|
||||
{t('admin.partnerDetail.campaigns.createNew')}
|
||||
@@ -100,6 +105,28 @@ export default function AdminPartnerCampaignAssign() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Show campaigns assigned to other partners as greyed out */}
|
||||
{takenByOthers.map((campaign) => (
|
||||
<div
|
||||
key={campaign.id}
|
||||
className="rounded-xl border border-dark-700/50 bg-dark-800 p-4 opacity-50"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-dark-300">{campaign.name}</span>
|
||||
<span className="rounded bg-purple-500/20 px-1.5 py-0.5 text-xs text-purple-400">
|
||||
{campaign.partner_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-dark-500">
|
||||
<span className="font-mono">?start={campaign.start_parameter}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { useState, useEffect, type FormEvent } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { partnerApi } from '../api/partners';
|
||||
import { campaignsApi, type CampaignBonusType } from '../api/campaigns';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/gi, '_')
|
||||
.replace(/^_|_$/g, '')
|
||||
.slice(0, 30);
|
||||
}
|
||||
|
||||
export default function AdminPartnerCampaignCreate() {
|
||||
const { t } = useTranslation();
|
||||
const { userId } = useParams<{ userId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: partner } = useQuery({
|
||||
queryKey: ['admin-partner-detail', userId],
|
||||
queryFn: () => partnerApi.getPartnerDetail(Number(userId)),
|
||||
enabled: !!userId,
|
||||
});
|
||||
|
||||
const partnerName = partner?.first_name || partner?.username || '';
|
||||
const suggestedParam = partnerName ? `partner_${slugify(partnerName)}` : '';
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [startParam, setStartParam] = useState('');
|
||||
const [bonusType, setBonusType] = useState<CampaignBonusType>('none');
|
||||
|
||||
// Auto-fill start_parameter when partner loads and fields are empty
|
||||
const [autoFilled, setAutoFilled] = useState(false);
|
||||
useEffect(() => {
|
||||
if (partner && !autoFilled) {
|
||||
if (!startParam) setStartParam(suggestedParam);
|
||||
if (!name && partnerName) setName(partnerName);
|
||||
setAutoFilled(true);
|
||||
}
|
||||
}, [partner, autoFilled, startParam, suggestedParam, name, partnerName]);
|
||||
|
||||
const createAndAssignMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const campaign = await campaignsApi.createCampaign({
|
||||
name,
|
||||
start_parameter: startParam,
|
||||
bonus_type: bonusType,
|
||||
is_active: true,
|
||||
});
|
||||
await partnerApi.assignCampaign(Number(userId), campaign.id);
|
||||
return campaign;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-partner-detail', userId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-campaigns-all'] });
|
||||
navigate(`/admin/partners/${userId}`);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !startParam.trim()) return;
|
||||
createAndAssignMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<AdminBackButton to={`/admin/partners/${userId}`} />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-dark-100">
|
||||
{t('admin.partnerDetail.campaigns.createTitle')}
|
||||
</h1>
|
||||
<p className="text-sm text-dark-400">
|
||||
{t('admin.partnerDetail.campaigns.createSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-4 rounded-xl border border-dark-700 bg-dark-800 p-4">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-dark-300">
|
||||
{t('admin.campaigns.form.name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 outline-none focus:border-accent-500"
|
||||
placeholder={t('admin.campaigns.form.namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Start Parameter */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-dark-300">
|
||||
{t('admin.campaigns.form.startParameter')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={startParam}
|
||||
onChange={(e) => setStartParam(e.target.value)}
|
||||
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 font-mono text-dark-100 outline-none focus:border-accent-500"
|
||||
placeholder="partner_name"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-xs text-dark-500">?start={startParam || '...'}</p>
|
||||
</div>
|
||||
|
||||
{/* Bonus Type */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-dark-300">
|
||||
{t('admin.campaigns.form.bonusType')}
|
||||
</label>
|
||||
<select
|
||||
value={bonusType}
|
||||
onChange={(e) => setBonusType(e.target.value as CampaignBonusType)}
|
||||
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 outline-none focus:border-accent-500"
|
||||
>
|
||||
<option value="none">{t('admin.campaigns.bonusType.none')}</option>
|
||||
<option value="balance">{t('admin.campaigns.bonusType.balance')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-assign notice */}
|
||||
<div className="rounded-xl border border-accent-500/20 bg-accent-500/5 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg
|
||||
className="mt-0.5 h-5 w-5 shrink-0 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-accent-300">
|
||||
{t('admin.partnerDetail.campaigns.autoAssignNote', {
|
||||
name: partnerName || `#${userId}`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{createAndAssignMutation.isError && (
|
||||
<div className="rounded-lg bg-error-500/10 p-3 text-sm text-error-400">
|
||||
{t('common.error')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/partners/${userId}`)}
|
||||
className="flex-1 rounded-lg bg-dark-700 px-4 py-3 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createAndAssignMutation.isPending || !name.trim() || !startParam.trim()}
|
||||
className="flex-1 rounded-lg bg-accent-500 px-4 py-3 font-medium text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
|
||||
>
|
||||
{createAndAssignMutation.isPending
|
||||
? t('common.saving')
|
||||
: t('admin.partnerDetail.campaigns.createAndAssign')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -216,7 +216,7 @@ export default function AdminPartnerDetail() {
|
||||
{t('admin.partnerDetail.campaigns.assign')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate(`/admin/partners/${userId}/campaigns/create`)}
|
||||
onClick={() => navigate(`/admin/campaigns/create?partnerId=${userId}`)}
|
||||
className="rounded-lg bg-accent-500/20 px-3 py-1.5 text-xs font-medium text-accent-400 transition-colors hover:bg-accent-500/30"
|
||||
>
|
||||
{t('admin.partnerDetail.campaigns.createNew')}
|
||||
|
||||
Reference in New Issue
Block a user