forked from wangziqi/gongxue-base
feat: add referral conversion report
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
loadPointExchangeItems,
|
||||
loadPointExchangeOrders,
|
||||
loadPointsRiskReport,
|
||||
loadReferralConversionReport,
|
||||
loadTenantMembers,
|
||||
loadUserNotifications,
|
||||
updateCommissionSettings,
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
type PointActivityTaskItem,
|
||||
type PointExchangeItem,
|
||||
type PointsRiskReport,
|
||||
type ReferralConversionReport,
|
||||
type TenantMemberItem,
|
||||
type UserNotificationAdminItem,
|
||||
} from '@/services/tenantAdmin';
|
||||
@@ -305,6 +307,7 @@ export default function TenantMarketingPage() {
|
||||
const [crmLogs, setCrmLogs] = useState<Record<string, CrmQueueLogItem[]>>({});
|
||||
const [commissionSettings, setCommissionSettings] = useState<CommissionSettingsItem | null>(null);
|
||||
const [commission, setCommission] = useState<CommissionSummaryItem | null>(null);
|
||||
const [conversionReport, setConversionReport] = useState<ReferralConversionReport | null>(null);
|
||||
const [commissionOrders, setCommissionOrders] = useState<CommissionOrderItem[]>([]);
|
||||
const [settlements, setSettlements] = useState<CommissionSettlementItem[]>([]);
|
||||
const [settlementProofs, setSettlementProofs] = useState<Record<string, CommissionSettlementProofItem[]>>({});
|
||||
@@ -380,6 +383,7 @@ export default function TenantMarketingPage() {
|
||||
crmDeadLettersPayload,
|
||||
commissionSettingsPayload,
|
||||
commissionPayload,
|
||||
conversionReportPayload,
|
||||
orderPayload,
|
||||
settlementPayload,
|
||||
memberPayload,
|
||||
@@ -410,6 +414,7 @@ export default function TenantMarketingPage() {
|
||||
loadCrmDeadLetters({ limit: 20 }).catch(() => ({ items: [], summary: {} })),
|
||||
loadCommissionSettings().catch(() => ({ item: null })),
|
||||
loadCommissionSummary(period).catch(() => ({ item: null })),
|
||||
loadReferralConversionReport({ ...period, limit: 20 }).catch(() => ({ item: null })),
|
||||
loadCommissionOrders({ ...period, limit: 20 }).catch(() => ({ items: [] })),
|
||||
loadCommissionSettlements({ limit: 20 }).catch(() => ({ items: [] })),
|
||||
loadTenantMembers({ limit: 100 }).catch(() => ({ items: [] })),
|
||||
@@ -438,6 +443,7 @@ export default function TenantMarketingPage() {
|
||||
setCrmDeadLetters(crmDeadLettersPayload || {});
|
||||
setCommissionSettings(nextSettings);
|
||||
setCommission(commissionPayload.item || null);
|
||||
setConversionReport(conversionReportPayload.item || null);
|
||||
setCommissionOrders(orderPayload.items || []);
|
||||
setSettlements(settlementPayload.items || []);
|
||||
setMembers(memberPayload.items || []);
|
||||
@@ -882,12 +888,14 @@ export default function TenantMarketingPage() {
|
||||
setBusy('commission');
|
||||
setError('');
|
||||
try {
|
||||
const [summaryPayload, orderPayload, settlementPayload] = await Promise.all([
|
||||
const [summaryPayload, conversionPayload, orderPayload, settlementPayload] = await Promise.all([
|
||||
loadCommissionSummary(period),
|
||||
loadReferralConversionReport({ ...period, limit: 30 }),
|
||||
loadCommissionOrders({ ...period, limit: 30 }),
|
||||
loadCommissionSettlements({ limit: 30 }),
|
||||
]);
|
||||
setCommission(summaryPayload.item || null);
|
||||
setConversionReport(conversionPayload.item || null);
|
||||
setCommissionOrders(orderPayload.items || []);
|
||||
setSettlements(settlementPayload.items || []);
|
||||
} catch (nextError) {
|
||||
@@ -1544,6 +1552,67 @@ export default function TenantMarketingPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>销售/代理转化</Text>
|
||||
<View className='admin-grid'>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>新增客资</Text><Text className='admin-metric-value'>{String(conversionReport?.summary?.leadCount || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>有效成交</Text><Text className='admin-metric-value'>{String(conversionReport?.summary?.convertedLeadCount || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>转化率</Text><Text className='admin-metric-value'>{percentLabel(conversionReport?.summary?.conversionRate)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>成交金额</Text><Text className='admin-metric-value'>{money(conversionReport?.summary?.grossAmountCents)}</Text></View>
|
||||
</View>
|
||||
<View className='admin-grid'>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>预计佣金</Text><Text className='admin-metric-value'>{money(conversionReport?.summary?.commissionAmountCents)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>CRM 失败</Text><Text className='admin-metric-value'>{String(conversionReport?.summary?.crmFailedCount || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>跟进积压</Text><Text className='admin-metric-value'>{String(conversionReport?.summary?.openFollowupCount || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>首付均时</Text><Text className='admin-metric-value'>{String(conversionReport?.summary?.avgFirstPayHours || 0)}h</Text></View>
|
||||
</View>
|
||||
<View className='admin-list compact'>
|
||||
{(conversionReport?.funnel || []).map(item => (
|
||||
<View className='admin-row' key={String(item.key || item.label)}>
|
||||
<Text className='admin-row-main'>{String(item.label || item.key || '漏斗')}</Text>
|
||||
<Text className='admin-row-meta'>{String(item.count || 0)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className='admin-list'>
|
||||
{(conversionReport?.byReferrer || []).slice(0, 8).map(item => (
|
||||
<View className='admin-row' key={item.referrerUserId}>
|
||||
<Text className='admin-row-main'>{item.name || item.phone || item.username || item.referrerUserId} · {money(item.grossAmountCents)}</Text>
|
||||
<Text className='admin-row-meta'>
|
||||
{item.role || 'member'} · 客资 {String(item.leadCount || 0)} · 成交 {String(item.convertedLeadCount || 0)} · 转化 {percentLabel(item.conversionRate)} · 佣金 {money(item.commissionAmountCents)}
|
||||
</Text>
|
||||
<Text className='admin-row-meta'>
|
||||
CRM 失败 {String(item.crmFailedCount || 0)} · 待跟进 {String(item.openFollowupCount || 0)} · 超期 {String(item.overdueFollowupCount || 0)} · 首付均时 {String(item.avgFirstPayHours || 0)}h
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{(conversionReport?.dailyTrend || []).length ? (
|
||||
<View className='admin-list compact'>
|
||||
{(conversionReport?.dailyTrend || []).slice(-7).map(item => (
|
||||
<View className='admin-row' key={String(item.date)}>
|
||||
<Text className='admin-row-main'>{String(item.date || '-')}</Text>
|
||||
<Text className='admin-row-meta'>客资 {String(item.leadCount || 0)} · 成交来源 {String(item.paidSourceCount || 0)} · 金额 {money(item.grossAmountCents)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
{(conversionReport?.recentUnconvertedLeads || []).length ? (
|
||||
<View className='admin-list'>
|
||||
<Text className='admin-section-title'>近期未成交客资</Text>
|
||||
{(conversionReport?.recentUnconvertedLeads || []).slice(0, 6).map(item => (
|
||||
<View className='admin-row' key={String(item.id)}>
|
||||
<Text className='admin-row-main'>{item.studentName || item.studentPhone || item.studentUserId || '学生'} · {item.referrerName || item.referrerPhone || item.referrerUserId || '归属人'}</Text>
|
||||
<Text className='admin-row-meta'>跟进 {item.assignedToName || item.assignedToUserId || '-'} · 来源 {item.source || '-'} · {shortDate(item.boundAt)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
{!(conversionReport?.byReferrer?.length || conversionReport?.recentUnconvertedLeads?.length) ? (
|
||||
<View className='admin-empty'>当前筛选范围暂无转化数据,或当前角色只能查看本人客资。</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>分佣订单明细</Text>
|
||||
<View className='admin-list'>
|
||||
|
||||
@@ -840,6 +840,86 @@ export interface CrmQueueLogItem {
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ReferralConversionReferrerItem {
|
||||
referrerUserId: string;
|
||||
role?: string;
|
||||
username?: string | null;
|
||||
name?: string | null;
|
||||
phone?: string | null;
|
||||
inviteCode?: string | null;
|
||||
trackCount?: number;
|
||||
shareCount?: number;
|
||||
intentCount?: number;
|
||||
leadCount?: number;
|
||||
assignedLeadCount?: number;
|
||||
convertedLeadCount?: number;
|
||||
paidLeadCount?: number;
|
||||
paidSourceCount?: number;
|
||||
grossAmountCents?: number;
|
||||
grossAmount?: number;
|
||||
commissionAmountCents?: number;
|
||||
commissionAmount?: number;
|
||||
firstPayUserCount?: number;
|
||||
avgFirstPayHours?: number;
|
||||
crmQueueCount?: number;
|
||||
crmSentCount?: number;
|
||||
crmFailedCount?: number;
|
||||
crmPendingCount?: number;
|
||||
followupCount?: number;
|
||||
openFollowupCount?: number;
|
||||
doneFollowupCount?: number;
|
||||
overdueFollowupCount?: number;
|
||||
conversionRate?: number;
|
||||
paidLeadRate?: number;
|
||||
}
|
||||
|
||||
export interface ReferralConversionReport {
|
||||
tenantId?: string;
|
||||
range?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
days?: number;
|
||||
referrerUserId?: string | null;
|
||||
};
|
||||
summary?: Partial<ReferralConversionReferrerItem> & {
|
||||
grossAmountCents?: number;
|
||||
commissionAmountCents?: number;
|
||||
grossAmount?: number;
|
||||
commissionAmount?: number;
|
||||
firstPayHoursSum?: number;
|
||||
};
|
||||
funnel?: Array<{
|
||||
key?: string;
|
||||
label?: string;
|
||||
count?: number;
|
||||
}>;
|
||||
byReferrer?: ReferralConversionReferrerItem[];
|
||||
dailyTrend?: Array<{
|
||||
date?: string;
|
||||
trackCount?: number;
|
||||
leadCount?: number;
|
||||
assignedLeadCount?: number;
|
||||
paidLeadCount?: number;
|
||||
paidSourceCount?: number;
|
||||
grossAmountCents?: number;
|
||||
commissionAmountCents?: number;
|
||||
}>;
|
||||
recentUnconvertedLeads?: Array<{
|
||||
id?: string;
|
||||
studentUserId?: string;
|
||||
studentName?: string | null;
|
||||
studentPhone?: string | null;
|
||||
referrerUserId?: string;
|
||||
referrerName?: string | null;
|
||||
referrerPhone?: string | null;
|
||||
assignedToUserId?: string | null;
|
||||
assignedToName?: string | null;
|
||||
refCode?: string | null;
|
||||
source?: string | null;
|
||||
boundAt?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CommissionSettingsItem {
|
||||
defaultRate?: number | string;
|
||||
minSettlementCents?: number;
|
||||
@@ -1699,6 +1779,17 @@ export async function loadCommissionSummary(query: { startDate?: string; endDate
|
||||
return apiRequest<{ item?: CommissionSummaryItem }>('/api/commission/summary', { query });
|
||||
}
|
||||
|
||||
export async function loadReferralConversionReport(query: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
referrerUserId?: string;
|
||||
limit?: number;
|
||||
} = {}) {
|
||||
return apiRequest<{ item?: ReferralConversionReport }>('/api/referral/conversion-report', {
|
||||
query: { ...query, limit: query.limit || 20 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCommissionOrders(query: { startDate?: string; endDate?: string; referrerUserId?: string; limit?: number } = {}) {
|
||||
return apiRequest<{ items?: CommissionOrderItem[]; scope?: Record<string, unknown> }>('/api/commission/orders', {
|
||||
query: { ...query, limit: query.limit || 50 },
|
||||
|
||||
Reference in New Issue
Block a user