feat: add referral conversion report

This commit is contained in:
Codex
2026-06-30 23:56:35 +08:00
parent 9891fe9ef3
commit 88640f8262
12 changed files with 899 additions and 25 deletions

View File

@@ -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'>