feat(cleaning): close settlement integrity and reconciliation
This commit is contained in:
@@ -826,6 +826,28 @@ export function listCleaningSettlements(
|
||||
return request<PageResult<CleaningSettlement>>(session, `/cleaning/settlements?${params}`);
|
||||
}
|
||||
|
||||
export function exportCleaningSettlements(
|
||||
session: ApiSession,
|
||||
input: {
|
||||
status?: SettlementStatus;
|
||||
payoutState?: PayoutStateFilter;
|
||||
storeId?: string;
|
||||
cleanerUserId?: string;
|
||||
}
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (input.status) params.set('status', input.status);
|
||||
if (input.payoutState) params.set('payoutState', input.payoutState);
|
||||
if (input.storeId) params.set('storeId', input.storeId);
|
||||
if (input.cleanerUserId) params.set('cleanerUserId', input.cleanerUserId);
|
||||
return request<{
|
||||
items: CleaningSettlement[];
|
||||
total: number;
|
||||
exported: number;
|
||||
truncated: boolean;
|
||||
}>(session, `/cleaning/settlements/export?${params}`);
|
||||
}
|
||||
|
||||
export function listCleaningSettlementCandidates(
|
||||
session: ApiSession,
|
||||
input: { page: number; pageSize: number; storeId?: string; cleanerUserId?: string }
|
||||
@@ -867,6 +889,32 @@ export function confirmCleaningSettlement(session: ApiSession, settlementId: str
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelCleaningSettlement(session: ApiSession, settlementId: string, reason: string) {
|
||||
return request<CleaningSettlement>(
|
||||
session,
|
||||
`/cleaning/settlements/${encodeURIComponent(settlementId)}/cancel`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function markCleaningSettlementPaid(
|
||||
session: ApiSession,
|
||||
settlementId: string,
|
||||
input: { payoutChannel: string; payoutReference: string; note?: string }
|
||||
) {
|
||||
return request<CleaningSettlement>(
|
||||
session,
|
||||
`/cleaning/settlements/${encodeURIComponent(settlementId)}/paid`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function executeWechatTransfer(
|
||||
session: ApiSession,
|
||||
settlementId: string,
|
||||
|
||||
@@ -56,7 +56,12 @@
|
||||
>
|
||||
批量同步
|
||||
</el-button>
|
||||
<el-button :icon="Download" :disabled="items.length === 0" @click="exportSettlements">
|
||||
<el-button
|
||||
:icon="Download"
|
||||
:disabled="total === 0"
|
||||
:loading="exporting"
|
||||
@click="exportSettlements"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button :icon="ListChecks" @click="openCandidates">
|
||||
@@ -108,7 +113,7 @@
|
||||
<el-table-column prop="createdAt" label="创建" width="130">
|
||||
<template #default="{ row }">{{ shortDate(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column label="操作" width="470" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button size="small" :icon="ListChecks" @click="openDetail(row)">详情</el-button>
|
||||
@@ -137,6 +142,14 @@
|
||||
>
|
||||
转账
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="BadgeCheck"
|
||||
:disabled="!canMarkPaid(row)"
|
||||
@click="openManualPaid(row)"
|
||||
>
|
||||
人工已付
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:icon="RefreshCw"
|
||||
@@ -149,11 +162,21 @@
|
||||
size="small"
|
||||
type="danger"
|
||||
:icon="CircleAlert"
|
||||
:disabled="row.status !== 'CONFIRMED'"
|
||||
:disabled="!canRecordFailure(row)"
|
||||
@click="openFailure(row)"
|
||||
>
|
||||
失败
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:icon="CircleX"
|
||||
:disabled="!canCancelSettlement(row)"
|
||||
@click="openCancel(row)"
|
||||
>
|
||||
冲正
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -390,7 +413,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailDialog.open" title="结算单详情" width="720px">
|
||||
<el-dialog v-model="detailDialog.open" title="结算单详情" width="880px">
|
||||
<div v-if="detailDialog.detail" class="dialog-stack">
|
||||
<div class="detail-grid">
|
||||
<span>单号<strong>{{ detailDialog.detail.settlement.settlementNo }}</strong></span>
|
||||
@@ -416,6 +439,9 @@
|
||||
<el-table-column prop="rewardCents" label="金额" width="110">
|
||||
<template #default="{ row }">{{ money(row.rewardCents) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="份额状态" width="100">
|
||||
<template #default="{ row }">{{ row.reversedAt ? '已冲正' : '有效' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completedAt" label="完成" width="130">
|
||||
<template #default="{ row }">{{ shortDate(row.completedAt) }}</template>
|
||||
</el-table-column>
|
||||
@@ -427,6 +453,9 @@
|
||||
<el-descriptions-item label="外部流水">
|
||||
{{ compactText(detailDialog.detail.settlement.payoutReference) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="付款请求号">
|
||||
{{ compactText(detailDialog.detail.settlement.payoutRequestNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信确认参数" :span="2">
|
||||
{{ compactText(detailDialog.detail.settlement.payoutPackageInfo) }}
|
||||
</el-descriptions-item>
|
||||
@@ -436,7 +465,45 @@
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ compactText(detailDialog.detail.settlement.note) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="冲正操作人">
|
||||
{{ compactText(detailDialog.detail.settlement.cancelledBy) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="冲正时间">
|
||||
{{ shortDate(detailDialog.detail.settlement.cancelledAt) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<section v-if="detailDialog.detail.reversals.length" class="detail-section">
|
||||
<h4>反向记录</h4>
|
||||
<el-table :data="detailDialog.detail.reversals" size="small">
|
||||
<el-table-column prop="reversalNo" label="反向单号" min-width="190" />
|
||||
<el-table-column label="金额(分)" width="120">
|
||||
<template #default="{ row }">{{ row.amountDeltaCents }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="原因" min-width="180" />
|
||||
<el-table-column prop="reversedBy" label="操作人" width="100" />
|
||||
<el-table-column prop="createdAt" label="时间" width="140">
|
||||
<template #default="{ row }">{{ shortDate(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>状态历史</h4>
|
||||
<el-table :data="detailDialog.detail.events" size="small">
|
||||
<el-table-column prop="action" label="动作" width="150" />
|
||||
<el-table-column label="状态" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ compactText(row.fromStatus) }} → {{ row.toStatus }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" width="130">
|
||||
<template #default="{ row }">{{ row.actorType }} · {{ row.actorId }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="170" />
|
||||
<el-table-column prop="createdAt" label="时间" width="140">
|
||||
<template #default="{ row }">{{ shortDate(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :icon="Download" :disabled="!detailDialog.detail" @click="exportSettlementDetail">
|
||||
@@ -452,6 +519,7 @@ import { computed, reactive, ref, watch } from 'vue';
|
||||
import {
|
||||
BadgeCheck,
|
||||
CircleAlert,
|
||||
CircleX,
|
||||
ClipboardCheck,
|
||||
Download,
|
||||
FilePlus2,
|
||||
@@ -460,8 +528,9 @@ import {
|
||||
Search,
|
||||
Send
|
||||
} from '@lucide/vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
exportCleaningSettlements,
|
||||
getCleaningSettlementDetail,
|
||||
listCleaningSettlementCandidates,
|
||||
preflightWechatTransfer
|
||||
@@ -502,6 +571,13 @@ const emit = defineEmits<{
|
||||
'sync-transfer': [{ settlementId: string; note?: string }];
|
||||
'sync-transfer-many': [{ settlementIds: string[]; note?: string }];
|
||||
failure: [{ settlementId: string; error: string; payoutReference?: string; note?: string }];
|
||||
cancel: [{ settlementId: string; reason: string }];
|
||||
paid: [{
|
||||
settlementId: string;
|
||||
payoutChannel: string;
|
||||
payoutReference: string;
|
||||
note?: string;
|
||||
}];
|
||||
}>();
|
||||
|
||||
const statusOptions = [
|
||||
@@ -524,6 +600,7 @@ const filterDraft = reactive({
|
||||
payoutState: props.filters.payoutState
|
||||
});
|
||||
const selectedSettlements = ref<CleaningSettlement[]>([]);
|
||||
const exporting = ref(false);
|
||||
const generateDialog = reactive({ open: false, cleanerUserId: '', storeId: '', note: '' });
|
||||
const transferDialog = reactive({
|
||||
open: false,
|
||||
@@ -616,6 +693,19 @@ function canSelectSettlement(row: CleaningSettlement) {
|
||||
return row.status === 'CONFIRMED';
|
||||
}
|
||||
|
||||
function canCancelSettlement(row: CleaningSettlement) {
|
||||
return (row.status === 'DRAFT' || row.status === 'CONFIRMED')
|
||||
&& !['PROCESSING', 'WAIT_USER_CONFIRM', 'SUCCESS'].includes(row.payoutState);
|
||||
}
|
||||
|
||||
function canMarkPaid(row: CleaningSettlement) {
|
||||
return row.status === 'CONFIRMED' && ['', 'FAIL'].includes(row.payoutState);
|
||||
}
|
||||
|
||||
function canRecordFailure(row: CleaningSettlement) {
|
||||
return row.status === 'CONFIRMED' && ['', 'FAIL'].includes(row.payoutState);
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: CleaningSettlement[]) {
|
||||
selectedSettlements.value = rows;
|
||||
}
|
||||
@@ -712,6 +802,51 @@ function submitFailure() {
|
||||
failureDialog.open = false;
|
||||
}
|
||||
|
||||
async function openCancel(row: CleaningSettlement) {
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
'结算单 ' + row.settlementNo + ' 将生成反向记录并释放任务份额,原单不会删除。',
|
||||
'冲正结算单',
|
||||
{
|
||||
confirmButtonText: '确认冲正',
|
||||
cancelButtonText: '返回',
|
||||
inputPlaceholder: '请输入冲正原因',
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '冲正原因不能为空',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
emit('cancel', { settlementId: row.id, reason: result.value.trim() });
|
||||
} catch {
|
||||
// Dialog cancellation is not an operation failure.
|
||||
}
|
||||
}
|
||||
|
||||
async function openManualPaid(row: CleaningSettlement) {
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
'结算单 ' + row.settlementNo + ' 将由人工渠道完成,请填写可对账的付款流水。',
|
||||
'标记人工已付',
|
||||
{
|
||||
confirmButtonText: '确认已付',
|
||||
cancelButtonText: '返回',
|
||||
inputPlaceholder: '付款流水号',
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '付款流水不能为空',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
emit('paid', {
|
||||
settlementId: row.id,
|
||||
payoutChannel: 'MANUAL',
|
||||
payoutReference: result.value.trim(),
|
||||
note: '后台人工确认付款完成'
|
||||
});
|
||||
} catch {
|
||||
// Dialog cancellation is not an operation failure.
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(row: CleaningSettlement) {
|
||||
detailDialog.open = true;
|
||||
detailDialog.loading = true;
|
||||
@@ -760,35 +895,51 @@ function useCandidateFilters() {
|
||||
candidateDialog.open = false;
|
||||
}
|
||||
|
||||
function exportSettlements() {
|
||||
downloadCsv(`cleaning-settlements-${Date.now()}.csv`, [
|
||||
['结算单', '状态', '保洁员', '门店', '任务数', '金额', '转账状态', '外部流水', '微信确认参数', '失败原因', '创建时间'],
|
||||
...props.items.map((item) => [
|
||||
item.settlementNo,
|
||||
item.status,
|
||||
item.cleanerName,
|
||||
item.storeName || '',
|
||||
String(item.taskCount),
|
||||
money(item.totalRewardCents),
|
||||
item.payoutState,
|
||||
item.payoutReference,
|
||||
item.payoutPackageInfo,
|
||||
item.payoutError,
|
||||
shortDate(item.createdAt)
|
||||
])
|
||||
]);
|
||||
async function exportSettlements() {
|
||||
exporting.value = true;
|
||||
try {
|
||||
const result = await exportCleaningSettlements(props.session, {
|
||||
status: props.status || undefined,
|
||||
payoutState: props.filters.payoutState || undefined,
|
||||
storeId: props.filters.storeId || undefined,
|
||||
cleanerUserId: props.filters.cleanerUserId || undefined
|
||||
});
|
||||
downloadCsv(`cleaning-settlements-${Date.now()}.csv`, [
|
||||
['结算单', '状态', '保洁员', '门店', '任务数', '金额(分)', '转账状态', '外部流水', '微信确认参数', '失败原因', '创建时间'],
|
||||
...result.items.map((item) => [
|
||||
item.settlementNo,
|
||||
item.status,
|
||||
item.cleanerName,
|
||||
item.storeName || '',
|
||||
String(item.taskCount),
|
||||
String(item.totalRewardCents),
|
||||
item.payoutState,
|
||||
item.payoutReference,
|
||||
item.payoutPackageInfo,
|
||||
item.payoutError,
|
||||
shortDate(item.createdAt)
|
||||
])
|
||||
]);
|
||||
if (result.truncated) {
|
||||
ElMessage.warning(`导出达到上限:${result.exported}/${result.total}`);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '结算导出失败');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function exportCandidates() {
|
||||
downloadCsv(`cleaning-settlement-candidates-${Date.now()}.csv`, [
|
||||
['任务号', '订单号', '门店', '房间', '保洁员ID', '金额', '完成时间'],
|
||||
['任务号', '订单号', '门店', '房间', '保洁员ID', '金额(分)', '完成时间'],
|
||||
...candidateDialog.items.map((item) => [
|
||||
item.taskNo,
|
||||
item.orderNo || '',
|
||||
item.storeName,
|
||||
item.roomName || item.roomNo,
|
||||
item.cleanerUserId || '',
|
||||
money(item.rewardCents),
|
||||
String(item.rewardCents),
|
||||
shortDate(item.completedAt)
|
||||
])
|
||||
]);
|
||||
@@ -805,8 +956,9 @@ function exportSettlementDetail() {
|
||||
['settlement', '保洁员', settlement.cleanerName],
|
||||
['settlement', '门店', settlement.storeName || ''],
|
||||
['settlement', '任务数', String(settlement.taskCount)],
|
||||
['settlement', '金额', money(settlement.totalRewardCents)],
|
||||
['settlement', '金额(分)', String(settlement.totalRewardCents)],
|
||||
['settlement', '转账状态', settlement.payoutState],
|
||||
['settlement', '付款请求号', settlement.payoutRequestNo],
|
||||
['settlement', '外部流水', settlement.payoutReference],
|
||||
['settlement', '微信确认参数', settlement.payoutPackageInfo],
|
||||
['settlement', '失败原因', settlement.payoutError],
|
||||
@@ -815,7 +967,17 @@ function exportSettlementDetail() {
|
||||
...detail.items.map((item) => [
|
||||
'item',
|
||||
item.taskNo,
|
||||
`${item.storeName} ${item.roomName || item.roomNo} ${item.cleanerName} ${money(item.rewardCents)} ${shortDate(item.completedAt)}`
|
||||
`${item.storeName} ${item.roomName || item.roomNo} ${item.cleanerName} ${item.rewardCents}分 ${item.reversedAt ? '已冲正' : '有效'} ${shortDate(item.completedAt)}`
|
||||
]),
|
||||
...detail.reversals.map((reversal) => [
|
||||
'reversal',
|
||||
reversal.reversalNo,
|
||||
`${reversal.amountDeltaCents}分 ${reversal.reason} ${reversal.reversedBy} ${shortDate(reversal.createdAt)}`
|
||||
]),
|
||||
...detail.events.map((event) => [
|
||||
'event',
|
||||
event.action,
|
||||
`${event.fromStatus || ''}->${event.toStatus} ${event.actorType}:${event.actorId} ${event.note} ${shortDate(event.createdAt)}`
|
||||
])
|
||||
]);
|
||||
}
|
||||
@@ -828,7 +990,7 @@ function exportPreflight() {
|
||||
['summary', 'ready', result.ready ? 'PASS' : 'BLOCKED'],
|
||||
['settlement', 'settlementNo', result.settlement.settlementNo],
|
||||
['settlement', 'status', result.settlement.status],
|
||||
['settlement', 'amount', money(result.settlement.totalRewardCents)],
|
||||
['settlement', 'amountCents', String(result.settlement.totalRewardCents)],
|
||||
['settlement', 'cleanerUserId', result.settlement.cleanerUserId],
|
||||
['settlement', 'storeId', result.settlement.storeId || ''],
|
||||
['account', 'merchantId', compactText(result.account.merchantIdMasked)],
|
||||
|
||||
@@ -288,7 +288,7 @@ function filterByCleaner(cleanerUserId: string) {
|
||||
|
||||
function exportCleanerPerformance() {
|
||||
downloadCsv(`cleaning-cleaner-performance-${Date.now()}.csv`, [
|
||||
['排名', '保洁员', '保洁员ID', '任务', '完成', '驳回', '完成率', '待结算', '已结算'],
|
||||
['排名', '保洁员', '保洁员ID', '任务', '完成', '驳回', '完成率', '待结算(分)', '已结算(分)'],
|
||||
...cleanerPerformanceRows.value.map((row) => [
|
||||
String(row.rank),
|
||||
row.cleanerName || '',
|
||||
@@ -297,8 +297,8 @@ function exportCleanerPerformance() {
|
||||
String(row.completedTaskCount),
|
||||
String(row.rejectedTaskCount),
|
||||
`${row.completionRate}%`,
|
||||
money(row.pendingSettlementCents),
|
||||
money(row.settledRewardCents)
|
||||
String(row.pendingSettlementCents),
|
||||
String(row.settledRewardCents)
|
||||
])
|
||||
]);
|
||||
}
|
||||
@@ -316,14 +316,14 @@ function exportStatistics() {
|
||||
['summary', '\u9a73\u56de', String(props.statistics.summary.rejected), '', '', '', ''],
|
||||
['summary', '\u514d\u6e05\u6d01', String(props.statistics.summary.exempted), '', '', '', ''],
|
||||
['summary', '\u5df2\u5b8c\u6210', String(props.statistics.summary.completed), '', '', '', ''],
|
||||
['summary', '\u5956\u52b1\u5408\u8ba1', money(props.statistics.summary.rewardCents), '', '', '', ''],
|
||||
['summary', '\u5f85\u7ed3\u7b97', money(props.statistics.summary.pendingSettlementCents), '', '', '', ''],
|
||||
['summary', '\u5f85\u53d1\u653e', money(props.statistics.summary.confirmedSettlementCents), '', '', '', ''],
|
||||
['summary', '\u5df2\u53d1\u653e', money(props.statistics.summary.paidSettlementCents), '', '', '', ''],
|
||||
['\u4efb\u52a1\u72b6\u6001', '\u72b6\u6001', '\u6570\u91cf', '\u5956\u52b1', '', '', ''],
|
||||
...props.statistics.byStatus.map((row) => ['\u4efb\u52a1\u72b6\u6001', row.status, String(row.total), money(row.rewardCents), '', '', '']),
|
||||
['\u7ed3\u7b97\u53e3\u5f84', '\u72b6\u6001', '\u5355\u6570', '\u91d1\u989d', '', '', ''],
|
||||
...props.statistics.settlements.map((row) => ['\u7ed3\u7b97\u53e3\u5f84', row.status, String(row.total), money(row.rewardCents), '', '', '']),
|
||||
['summary', '\u5956\u52b1\u5408\u8ba1(\u5206)', String(props.statistics.summary.rewardCents), '', '', '', ''],
|
||||
['summary', '\u5f85\u7ed3\u7b97(\u5206)', String(props.statistics.summary.pendingSettlementCents), '', '', '', ''],
|
||||
['summary', '\u5f85\u53d1\u653e(\u5206)', String(props.statistics.summary.confirmedSettlementCents), '', '', '', ''],
|
||||
['summary', '\u5df2\u53d1\u653e(\u5206)', String(props.statistics.summary.paidSettlementCents), '', '', '', ''],
|
||||
['\u4efb\u52a1\u72b6\u6001', '\u72b6\u6001', '\u6570\u91cf', '\u5956\u52b1(\u5206)', '', '', ''],
|
||||
...props.statistics.byStatus.map((row) => ['\u4efb\u52a1\u72b6\u6001', row.status, String(row.total), String(row.rewardCents), '', '', '']),
|
||||
['\u7ed3\u7b97\u53e3\u5f84', '\u72b6\u6001', '\u5355\u6570', '\u91d1\u989d(\u5206)', '', '', ''],
|
||||
...props.statistics.settlements.map((row) => ['\u7ed3\u7b97\u53e3\u5f84', row.status, String(row.total), String(row.rewardCents), '', '', '']),
|
||||
['\u6bcf\u65e5\u8d8b\u52bf', '\u65e5\u671f', '\u4efb\u52a1', '\u5f85\u9a8c', '\u5b8c\u6210', '\u9a73\u56de', '\u514d\u6e05\u6d01'],
|
||||
...props.statistics.trend.map((row) => [
|
||||
'\u6bcf\u65e5\u8d8b\u52bf',
|
||||
@@ -334,8 +334,8 @@ function exportStatistics() {
|
||||
String(row.rejected),
|
||||
String(row.exempted)
|
||||
]),
|
||||
['\u6bcf\u65e5\u53d1\u653e', '\u65e5\u671f', '\u5df2\u53d1\u653e', '', '', '', ''],
|
||||
...props.statistics.trend.map((row) => ['\u6bcf\u65e5\u53d1\u653e', row.date, money(row.paidSettlementCents), '', '', '', '']),
|
||||
['\u6bcf\u65e5\u53d1\u653e', '\u65e5\u671f', '\u5df2\u53d1\u653e(\u5206)', '', '', '', ''],
|
||||
...props.statistics.trend.map((row) => ['\u6bcf\u65e5\u53d1\u653e', row.date, String(row.paidSettlementCents), '', '', '', '']),
|
||||
['\u95e8\u5e97\u660e\u7ec6', '\u95e8\u5e97', '\u4efb\u52a1', '\u5f85\u9a8c', '\u5b8c\u6210', '\u9a73\u56de', '\u514d\u6e05\u6d01'],
|
||||
...props.statistics.byStore.map((row) => [
|
||||
'\u95e8\u5e97\u660e\u7ec6',
|
||||
@@ -346,19 +346,19 @@ function exportStatistics() {
|
||||
String(row.rejected),
|
||||
String(row.exempted)
|
||||
]),
|
||||
['\u95e8\u5e97\u5956\u52b1', '\u95e8\u5e97', '\u5956\u52b1', '', '', '', ''],
|
||||
...props.statistics.byStore.map((row) => ['\u95e8\u5e97\u5956\u52b1', `${row.storeName || ''}(${row.storeId})`, money(row.rewardCents), '', '', '', '']),
|
||||
['\u6210\u5458\u5206\u8d26', '\u4fdd\u6d01\u5458', '\u4efb\u52a1', '\u5b8c\u6210', '\u9a73\u56de', '\u5f85\u7ed3\u7b97', '\u5df2\u7ed3\u7b97'],
|
||||
['\u95e8\u5e97\u5956\u52b1', '\u95e8\u5e97', '\u5956\u52b1(\u5206)', '', '', '', ''],
|
||||
...props.statistics.byStore.map((row) => ['\u95e8\u5e97\u5956\u52b1', `${row.storeName || ''}(${row.storeId})`, String(row.rewardCents), '', '', '', '']),
|
||||
['\u6210\u5458\u5206\u8d26', '\u4fdd\u6d01\u5458', '\u4efb\u52a1', '\u5b8c\u6210', '\u9a73\u56de', '\u5f85\u7ed3\u7b97(\u5206)', '\u5df2\u7ed3\u7b97(\u5206)'],
|
||||
...props.statistics.members.map((row) => [
|
||||
'\u6210\u5458\u5206\u8d26',
|
||||
`${row.cleanerName || ''}(${row.cleanerUserId})`,
|
||||
String(row.taskCount),
|
||||
String(row.completedTaskCount),
|
||||
String(row.rejectedTaskCount),
|
||||
money(row.pendingSettlementCents),
|
||||
money(row.settledRewardCents)
|
||||
String(row.pendingSettlementCents),
|
||||
String(row.settledRewardCents)
|
||||
]),
|
||||
['\u7ee9\u6548\u6392\u884c', '\u6392\u540d', '\u4fdd\u6d01\u5458', '\u4efb\u52a1', '\u5b8c\u6210\u7387', '\u9a73\u56de', '\u7ed3\u7b97\u5408\u8ba1'],
|
||||
['\u7ee9\u6548\u6392\u884c', '\u6392\u540d', '\u4fdd\u6d01\u5458', '\u4efb\u52a1', '\u5b8c\u6210\u7387', '\u9a73\u56de', '\u7ed3\u7b97\u5408\u8ba1(\u5206)'],
|
||||
...cleanerPerformanceRows.value.map((row) => [
|
||||
'\u7ee9\u6548\u6392\u884c',
|
||||
String(row.rank),
|
||||
@@ -366,7 +366,7 @@ function exportStatistics() {
|
||||
String(row.taskCount),
|
||||
`${row.completionRate}%`,
|
||||
String(row.rejectedTaskCount),
|
||||
money(row.settlementTotalCents)
|
||||
String(row.settlementTotalCents)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -791,7 +791,7 @@ async function removeMember(cleanerUserId: string) {
|
||||
|
||||
function exportTasks() {
|
||||
downloadCsv(`cleaning-tasks-${Date.now()}.csv`, [
|
||||
['任务号', '状态', '门店', '房间', '订单号', '保洁员ID', '协作人数', '奖励金额', '照片数', '提交时间', '完成时间', '驳回原因'],
|
||||
['任务号', '状态', '门店', '房间', '订单号', '保洁员ID', '协作人数', '奖励金额(分)', '照片数', '提交时间', '完成时间', '驳回原因'],
|
||||
...props.items.map((item) => [
|
||||
item.taskNo,
|
||||
item.status,
|
||||
@@ -800,7 +800,7 @@ function exportTasks() {
|
||||
item.orderNo || '',
|
||||
item.cleanerUserId || '',
|
||||
String(item.memberCount || 0),
|
||||
money(item.rewardCents),
|
||||
String(item.rewardCents),
|
||||
String(item.photoUrls?.length || 0),
|
||||
shortDate(item.submittedAt),
|
||||
shortDate(item.completedAt),
|
||||
@@ -820,7 +820,7 @@ function exportTaskDetail() {
|
||||
['task', '房间', task.roomName || task.roomNo],
|
||||
['task', '订单号', task.orderNo || ''],
|
||||
['task', '保洁员ID', task.cleanerUserId || ''],
|
||||
['task', '奖励金额', money(task.rewardCents)],
|
||||
['task', '奖励金额(分)', String(task.rewardCents)],
|
||||
['task', '照片数', String(task.photoUrls?.length || 0)],
|
||||
['task', '领取时间', shortDate(task.claimedAt)],
|
||||
['task', '开始时间', shortDate(task.startedAt)],
|
||||
@@ -831,7 +831,7 @@ function exportTaskDetail() {
|
||||
...detailDialog.members.map((member) => [
|
||||
'member',
|
||||
`${member.memberRole}:${member.userId}`,
|
||||
`${member.nickname || ''} ${money(member.rewardCents)} ${member.settledAt ? shortDate(member.settledAt) : '未结算'}`
|
||||
`${member.nickname || ''} ${member.rewardCents}分 ${member.settledAt ? shortDate(member.settledAt) : '未结算'}`
|
||||
]),
|
||||
...detailDialog.events.map((event) => [
|
||||
'event',
|
||||
@@ -873,13 +873,13 @@ function exportTaskEvents() {
|
||||
|
||||
function exportTaskMembers() {
|
||||
downloadCsv(`cleaning-task-members-${memberDialog.taskNo}-${Date.now()}.csv`, [
|
||||
['任务号', '成员ID', '昵称', '角色', '分账金额', '结算时间'],
|
||||
['任务号', '成员ID', '昵称', '角色', '分账金额(分)', '结算时间'],
|
||||
...memberDialog.members.map((member) => [
|
||||
memberDialog.taskNo,
|
||||
member.userId,
|
||||
member.nickname || '',
|
||||
member.memberRole,
|
||||
money(member.rewardCents),
|
||||
String(member.rewardCents),
|
||||
shortDate(member.settledAt)
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -113,6 +113,8 @@
|
||||
@sync-transfer="handleSyncTransfer"
|
||||
@sync-transfer-many="handleSyncTransferMany"
|
||||
@failure="handleFailure"
|
||||
@cancel="handleCancelSettlement"
|
||||
@paid="handleManualPaid"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="统计" name="statistics">
|
||||
@@ -167,6 +169,7 @@ import CleaningFieldHandoffPanel from './CleaningFieldHandoffPanel.vue';
|
||||
import {
|
||||
ApiError,
|
||||
assignCleaningTask,
|
||||
cancelCleaningSettlement,
|
||||
completeCleaningTask,
|
||||
confirmCleaningSettlement,
|
||||
createStaffUser,
|
||||
@@ -177,6 +180,7 @@ import {
|
||||
listCleaningSettlements,
|
||||
listCleaningTasks,
|
||||
listStaffUsers,
|
||||
markCleaningSettlementPaid,
|
||||
reclaimCleaningTimeouts,
|
||||
recordPayoutFailure,
|
||||
rejectCleaningTask,
|
||||
@@ -234,6 +238,7 @@ const statistics = reactive<CleaningStatistics>({
|
||||
completed: 0,
|
||||
rewardCents: 0,
|
||||
pendingSettlementCents: 0,
|
||||
draftSettlementCents: 0,
|
||||
confirmedSettlementCents: 0,
|
||||
paidSettlementCents: 0
|
||||
},
|
||||
@@ -372,12 +377,12 @@ function jumpToSettlementStatus(status: SettlementStatus) {
|
||||
|
||||
function exportOperationalQueue() {
|
||||
downloadCsv(`cleaning-operational-queue-${Date.now()}.csv`, [
|
||||
['队列', '数量', '金额', '入口'],
|
||||
['队列', '数量', '金额(分)', '入口'],
|
||||
['待验收任务', String(statistics.summary.pendingReview), '', '任务/SUBMITTED'],
|
||||
['驳回补做', String(statistics.summary.rejected), '', '任务/REJECTED'],
|
||||
['待确认结算', String(draftSettlementTotal.value), '', '结算/DRAFT'],
|
||||
['待发放金额', '', money(statistics.summary.confirmedSettlementCents), '结算/CONFIRMED'],
|
||||
['待结算金额', '', money(statistics.summary.pendingSettlementCents), '统计/待结算'],
|
||||
['待发放金额', '', String(statistics.summary.confirmedSettlementCents), '结算/CONFIRMED'],
|
||||
['待结算金额', '', String(statistics.summary.pendingSettlementCents), '统计/待结算'],
|
||||
['已完成任务', String(statistics.summary.completed), '', '统计/已完成']
|
||||
]);
|
||||
}
|
||||
@@ -519,6 +524,29 @@ async function handleConfirmSettlement(input: { settlementId: string; note?: str
|
||||
}, '已确认结算单');
|
||||
}
|
||||
|
||||
async function handleCancelSettlement(input: { settlementId: string; reason: string }) {
|
||||
await runAction(async () => {
|
||||
await cancelCleaningSettlement(session.value, input.settlementId, input.reason);
|
||||
await Promise.all([loadTasks(), loadSettlements(), loadStatistics()]);
|
||||
}, '结算单已冲正');
|
||||
}
|
||||
|
||||
async function handleManualPaid(input: {
|
||||
settlementId: string;
|
||||
payoutChannel: string;
|
||||
payoutReference: string;
|
||||
note?: string;
|
||||
}) {
|
||||
await runAction(async () => {
|
||||
await markCleaningSettlementPaid(session.value, input.settlementId, {
|
||||
payoutChannel: input.payoutChannel,
|
||||
payoutReference: input.payoutReference,
|
||||
note: input.note
|
||||
});
|
||||
await Promise.all([loadTasks(), loadSettlements(), loadStatistics()]);
|
||||
}, '人工付款已完成');
|
||||
}
|
||||
|
||||
async function handleTransfer(input: { settlementId: string; mode: TransferMode; note?: string }) {
|
||||
await runAction(async () => {
|
||||
await executeWechatTransfer(session.value, input.settlementId, {
|
||||
|
||||
@@ -673,6 +673,7 @@ export interface CleaningTaskEvent {
|
||||
toStatus: TaskStatus;
|
||||
action: string;
|
||||
actorId: string;
|
||||
actorType: 'USER' | 'SYSTEM';
|
||||
traceId: string;
|
||||
note: string;
|
||||
createdAt: string;
|
||||
@@ -705,9 +706,12 @@ export interface CleaningSettlement {
|
||||
periodStart: string | null;
|
||||
periodEnd: string | null;
|
||||
paidBy: string | null;
|
||||
cancelledBy: string | null;
|
||||
confirmedAt: string | null;
|
||||
paidAt: string | null;
|
||||
cancelledAt: string | null;
|
||||
payoutChannel: string;
|
||||
payoutRequestNo: string;
|
||||
payoutReference: string;
|
||||
payoutState: string;
|
||||
payoutPackageInfo: string;
|
||||
@@ -728,13 +732,39 @@ export interface CleaningSettlementItem {
|
||||
cleanerUserId: string;
|
||||
cleanerName: string;
|
||||
rewardCents: number;
|
||||
reversedAt: string | null;
|
||||
completedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CleaningSettlementEvent {
|
||||
id: string;
|
||||
settlementId: string;
|
||||
fromStatus: SettlementStatus | null;
|
||||
toStatus: SettlementStatus;
|
||||
action: string;
|
||||
actorType: 'USER' | 'SYSTEM';
|
||||
actorId: string;
|
||||
traceId: string;
|
||||
note: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CleaningSettlementReversal {
|
||||
id: string;
|
||||
settlementId: string;
|
||||
reversalNo: string;
|
||||
amountDeltaCents: number;
|
||||
reason: string;
|
||||
reversedBy: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CleaningSettlementDetail {
|
||||
settlement: CleaningSettlement;
|
||||
items: CleaningSettlementItem[];
|
||||
events: CleaningSettlementEvent[];
|
||||
reversals: CleaningSettlementReversal[];
|
||||
}
|
||||
|
||||
export interface CleaningStatistics {
|
||||
@@ -747,6 +777,7 @@ export interface CleaningStatistics {
|
||||
completed: number;
|
||||
rewardCents: number;
|
||||
pendingSettlementCents: number;
|
||||
draftSettlementCents: number;
|
||||
confirmedSettlementCents: number;
|
||||
paidSettlementCents: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user