forked from wangziqi/gongxue-base
feat: add commission settlement exports and proofs
This commit is contained in:
@@ -2,10 +2,13 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Input, Text, View } from '@tarojs/components';
|
||||
import {
|
||||
createCommissionSettlementProof,
|
||||
exportCommissionSettlement,
|
||||
generateCommissionSettlement,
|
||||
loadActivationCodes,
|
||||
loadCodeBatches,
|
||||
loadCommissionOrders,
|
||||
loadCommissionSettlementProofs,
|
||||
loadCommissionSettings,
|
||||
loadCommissionSettlements,
|
||||
loadCommissionSummary,
|
||||
@@ -14,11 +17,13 @@ import {
|
||||
loadCrmQueue,
|
||||
loadTenantMembers,
|
||||
updateCommissionSettings,
|
||||
updateCommissionSettlementProofStatus,
|
||||
updateCommissionSettlementStatus,
|
||||
updateMemberCommissionRate,
|
||||
upsertCrmConfig,
|
||||
type CodeBatchItem,
|
||||
type CommissionOrderItem,
|
||||
type CommissionSettlementProofItem,
|
||||
type CommissionSettlementItem,
|
||||
type CommissionSettingsItem,
|
||||
type CommissionSummaryItem,
|
||||
@@ -51,6 +56,17 @@ function rateToPercent(value: unknown) {
|
||||
return String((Number(value || 0) * 100).toFixed(2)).replace(/\.00$/, '');
|
||||
}
|
||||
|
||||
function downloadBase64File(filename: string, contentBase64: string, mimeType: string) {
|
||||
if (typeof document === 'undefined') return false;
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:${mimeType};base64,${contentBase64}`;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function TenantMarketingPage() {
|
||||
const [coupons, setCoupons] = useState<CouponItem[]>([]);
|
||||
const [batches, setBatches] = useState<CodeBatchItem[]>([]);
|
||||
@@ -61,6 +77,7 @@ export default function TenantMarketingPage() {
|
||||
const [commission, setCommission] = useState<CommissionSummaryItem | null>(null);
|
||||
const [commissionOrders, setCommissionOrders] = useState<CommissionOrderItem[]>([]);
|
||||
const [settlements, setSettlements] = useState<CommissionSettlementItem[]>([]);
|
||||
const [settlementProofs, setSettlementProofs] = useState<Record<string, CommissionSettlementProofItem[]>>({});
|
||||
const [members, setMembers] = useState<TenantMemberItem[]>([]);
|
||||
const [crmForm, setCrmForm] = useState({
|
||||
enabled: false,
|
||||
@@ -77,6 +94,7 @@ export default function TenantMarketingPage() {
|
||||
const [period, setPeriod] = useState({ startDate: monthStart(), endDate: today(), referrerUserId: '' });
|
||||
const [memberRate, setMemberRate] = useState({ userId: '', ratePercent: '' });
|
||||
const [settlementPay, setSettlementPay] = useState({ paymentMethod: 'offline_bank', paymentAccount: '' });
|
||||
const [proofForm, setProofForm] = useState({ externalUrl: '', title: '', reviewNote: '' });
|
||||
const [crmStatus, setCrmStatus] = useState('');
|
||||
const [busy, setBusy] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -292,6 +310,82 @@ export default function TenantMarketingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportSettlement(item: CommissionSettlementItem) {
|
||||
setBusy(`settlement:${item.id}:export`);
|
||||
setError('');
|
||||
try {
|
||||
const payload = await exportCommissionSettlement(item.id, 'csv');
|
||||
const exportItem = payload.item;
|
||||
if (exportItem?.contentBase64 && exportItem.filename) {
|
||||
const downloaded = downloadBase64File(exportItem.filename, exportItem.contentBase64, exportItem.mimeType || 'text/csv');
|
||||
Taro.showToast({ title: downloaded ? '导出已下载' : '导出已生成', icon: 'success' });
|
||||
}
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '结算导出失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitProof(item: CommissionSettlementItem) {
|
||||
if (!proofForm.externalUrl.trim()) {
|
||||
Taro.showToast({ title: '请填写凭证链接', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setBusy(`settlement:${item.id}:proof`);
|
||||
setError('');
|
||||
try {
|
||||
await createCommissionSettlementProof({
|
||||
settlementId: item.id,
|
||||
proofType: 'payment',
|
||||
title: proofForm.title.trim() || `${item.settlementNo || item.id} 打款凭证`,
|
||||
externalUrl: proofForm.externalUrl.trim(),
|
||||
amountCents: item.commissionAmountCents || null,
|
||||
paymentMethod: settlementPay.paymentMethod || null,
|
||||
paymentAccount: settlementPay.paymentAccount || null,
|
||||
metadata: { source: 'taro-tenant-admin' },
|
||||
});
|
||||
Taro.showToast({ title: '凭证已登记', icon: 'success' });
|
||||
await refreshProofs(item.id);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '凭证登记失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshProofs(settlementId: string) {
|
||||
setBusy(`settlement:${settlementId}:proofs`);
|
||||
setError('');
|
||||
try {
|
||||
const payload = await loadCommissionSettlementProofs(settlementId);
|
||||
setSettlementProofs(prev => ({ ...prev, [settlementId]: payload.items || [] }));
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '凭证加载失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function reviewProof(proof: CommissionSettlementProofItem, status: string) {
|
||||
setBusy(`proof:${proof.id}:${status}`);
|
||||
setError('');
|
||||
try {
|
||||
await updateCommissionSettlementProofStatus({
|
||||
proofId: proof.id,
|
||||
status,
|
||||
reviewNote: proofForm.reviewNote.trim() || null,
|
||||
metadata: { source: 'taro-tenant-admin' },
|
||||
});
|
||||
Taro.showToast({ title: '凭证状态已更新', icon: 'success' });
|
||||
if (proof.settlementId) await refreshProofs(proof.settlementId);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '凭证复核失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='admin-page'>
|
||||
<View className='admin-shell'>
|
||||
@@ -455,20 +549,39 @@ export default function TenantMarketingPage() {
|
||||
<View className='admin-form-grid'>
|
||||
<Input className='admin-input' placeholder='打款方式,例如 offline_bank' value={settlementPay.paymentMethod} onInput={event => setSettlementPay(prev => ({ ...prev, paymentMethod: String(event.detail.value || '') }))} />
|
||||
<Input className='admin-input' placeholder='打款账户备注,可填脱敏账号' value={settlementPay.paymentAccount} onInput={event => setSettlementPay(prev => ({ ...prev, paymentAccount: String(event.detail.value || '') }))} />
|
||||
<Input className='admin-input' placeholder='凭证链接,例如对象存储签名后的公开凭证或内部 URL' value={proofForm.externalUrl} onInput={event => setProofForm(prev => ({ ...prev, externalUrl: String(event.detail.value || '') }))} />
|
||||
<Input className='admin-input' placeholder='凭证标题' value={proofForm.title} onInput={event => setProofForm(prev => ({ ...prev, title: String(event.detail.value || '') }))} />
|
||||
<Input className='admin-input' placeholder='复核备注' value={proofForm.reviewNote} onInput={event => setProofForm(prev => ({ ...prev, reviewNote: String(event.detail.value || '') }))} />
|
||||
</View>
|
||||
<View className='admin-list'>
|
||||
{settlements.map(item => (
|
||||
<View className='admin-row' key={item.id}>
|
||||
<Text className='admin-row-main'>{item.settlementNo || item.id}</Text>
|
||||
<Text className='admin-row-meta'>{item.referrerName || item.referrerPhone || item.referrerUserId} · {item.status || 'draft'} · {money(item.commissionAmountCents)}</Text>
|
||||
<Text className='admin-row-meta'>{item.periodStart || '-'} 至 {item.periodEnd || '-'} · 来源 {item.sourceCount || 0}</Text>
|
||||
<View className='admin-row-actions'>
|
||||
{item.status === 'pending_review' ? <Button className='admin-mini-button primary' loading={busy === `settlement:${item.id}:approved`} onClick={() => updateSettlement(item, 'approved')}>审核通过</Button> : null}
|
||||
{item.status === 'pending_review' ? <Button className='admin-mini-button' loading={busy === `settlement:${item.id}:rejected`} onClick={() => updateSettlement(item, 'rejected')}>驳回</Button> : null}
|
||||
{item.status === 'approved' ? <Button className='admin-mini-button primary' loading={busy === `settlement:${item.id}:paid`} onClick={() => updateSettlement(item, 'paid')}>标记打款</Button> : null}
|
||||
{settlements.map(item => {
|
||||
const proofs = settlementProofs[item.id] || [];
|
||||
return (
|
||||
<View className='admin-row' key={item.id}>
|
||||
<Text className='admin-row-main'>{item.settlementNo || item.id}</Text>
|
||||
<Text className='admin-row-meta'>{item.referrerName || item.referrerPhone || item.referrerUserId} · {item.status || 'draft'} · {money(item.commissionAmountCents)}</Text>
|
||||
<Text className='admin-row-meta'>{item.periodStart || '-'} 至 {item.periodEnd || '-'} · 来源 {item.sourceCount || 0} · 凭证 {proofs.length}</Text>
|
||||
<View className='admin-row-actions'>
|
||||
<Button className='admin-mini-button' loading={busy === `settlement:${item.id}:export`} onClick={() => exportSettlement(item)}>导出 CSV</Button>
|
||||
<Button className='admin-mini-button' loading={busy === `settlement:${item.id}:proofs`} onClick={() => refreshProofs(item.id)}>查看凭证</Button>
|
||||
{item.status === 'pending_review' ? <Button className='admin-mini-button primary' loading={busy === `settlement:${item.id}:approved`} onClick={() => updateSettlement(item, 'approved')}>审核通过</Button> : null}
|
||||
{item.status === 'pending_review' ? <Button className='admin-mini-button' loading={busy === `settlement:${item.id}:rejected`} onClick={() => updateSettlement(item, 'rejected')}>驳回</Button> : null}
|
||||
{item.status === 'approved' ? <Button className='admin-mini-button primary' loading={busy === `settlement:${item.id}:paid`} onClick={() => updateSettlement(item, 'paid')}>标记打款</Button> : null}
|
||||
{item.status === 'paid' ? <Button className='admin-mini-button primary' loading={busy === `settlement:${item.id}:proof`} onClick={() => submitProof(item)}>登记凭证</Button> : null}
|
||||
</View>
|
||||
{proofs.map(proof => (
|
||||
<View className='admin-row nested' key={proof.id}>
|
||||
<Text className='admin-row-main'>{proof.title || proof.proofType || '凭证'} · {proof.status || 'submitted'}</Text>
|
||||
<Text className='admin-row-meta'>{proof.externalUrl || proof.assetId || '-'} · {money(proof.amountCents)}</Text>
|
||||
<View className='admin-row-actions'>
|
||||
{proof.status === 'submitted' ? <Button className='admin-mini-button primary' loading={busy === `proof:${proof.id}:approved`} onClick={() => reviewProof(proof, 'approved')}>凭证通过</Button> : null}
|
||||
{proof.status === 'submitted' ? <Button className='admin-mini-button' loading={busy === `proof:${proof.id}:rejected`} onClick={() => reviewProof(proof, 'rejected')}>凭证驳回</Button> : null}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
{!settlements.length ? <View className='admin-empty'>暂无结算单。</View> : null}
|
||||
</View>
|
||||
|
||||
@@ -511,6 +511,41 @@ export interface CommissionSettlementItem {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CommissionSettlementExportItem {
|
||||
settlementId?: string;
|
||||
filename?: string;
|
||||
format?: 'csv' | 'json';
|
||||
mimeType?: string;
|
||||
rowCount?: number;
|
||||
contentBase64?: string;
|
||||
sha256?: string;
|
||||
sizeBytes?: number;
|
||||
}
|
||||
|
||||
export interface CommissionSettlementProofItem {
|
||||
id: string;
|
||||
settlementId?: string;
|
||||
proofType?: string;
|
||||
status?: string;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
assetId?: string | null;
|
||||
externalUrl?: string | null;
|
||||
amountCents?: number | null;
|
||||
paymentMethod?: string | null;
|
||||
paymentAccount?: string | null;
|
||||
paidAt?: string | null;
|
||||
submittedBy?: string | null;
|
||||
submittedByName?: string | null;
|
||||
reviewedBy?: string | null;
|
||||
reviewedByName?: string | null;
|
||||
reviewedAt?: string | null;
|
||||
reviewNote?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TenantOverview {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -1078,6 +1113,18 @@ export async function loadCommissionSettlements(query: { status?: string; referr
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportCommissionSettlement(settlementId: string, format: 'csv' | 'json' = 'csv') {
|
||||
return apiRequest<{ item?: CommissionSettlementExportItem }>('/api/commission/settlements/export', {
|
||||
query: { settlementId, format },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCommissionSettlementProofs(settlementId: string) {
|
||||
return apiRequest<{ items?: CommissionSettlementProofItem[] }>('/api/commission/settlements/proofs', {
|
||||
query: { settlementId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateCommissionSettlement(input: {
|
||||
referrerUserId: string;
|
||||
startDate: string;
|
||||
@@ -1104,3 +1151,34 @@ export async function updateCommissionSettlementStatus(input: {
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCommissionSettlementProof(input: {
|
||||
settlementId: string;
|
||||
proofType?: string;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
assetId?: string | null;
|
||||
externalUrl?: string | null;
|
||||
amountCents?: number | null;
|
||||
paymentMethod?: string | null;
|
||||
paymentAccount?: string | null;
|
||||
paidAt?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}) {
|
||||
return apiRequest<{ item?: CommissionSettlementProofItem }>('/api/commission/settlements/proofs', {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCommissionSettlementProofStatus(input: {
|
||||
proofId: string;
|
||||
status: string;
|
||||
reviewNote?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}) {
|
||||
return apiRequest<{ item?: CommissionSettlementProofItem }>('/api/commission/settlements/proofs/status', {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user