增加接单工单筛选统计
This commit is contained in:
@@ -253,6 +253,14 @@ export type ListInput = {
|
||||
sort?: 'id_desc' | 'worker_claimed_at_desc'
|
||||
}
|
||||
|
||||
export type WorkOrderStatistics = {
|
||||
totalCount: number
|
||||
userPaymentAmount: number
|
||||
rewardAmount: number
|
||||
refundAmount: number
|
||||
statusCounts: Record<string, number>
|
||||
}
|
||||
|
||||
export type ProductRuleListInput = {
|
||||
enabled?: boolean | null
|
||||
keyword?: string
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
WorkerDepositUnfreezeRow,
|
||||
WorkOrderRow,
|
||||
WorkOrderShareRow,
|
||||
WorkOrderStatistics,
|
||||
WorkProductRuleRow,
|
||||
} from './types.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
@@ -56,6 +57,14 @@ type WorkerOrderOverviewRow = {
|
||||
vip_evidence_pending_settlement_amount: number
|
||||
}
|
||||
|
||||
type WorkOrderStatisticsRow = {
|
||||
status: string
|
||||
total_count: number
|
||||
user_payment_amount: number | string
|
||||
reward_amount: number | string
|
||||
refund_amount: number | string
|
||||
}
|
||||
|
||||
export async function listWorkOrderShares(
|
||||
workOrderId: number | string,
|
||||
): Promise<WorkOrderShareRow[]> {
|
||||
@@ -830,6 +839,64 @@ export async function listWorkOrders({
|
||||
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
||||
}
|
||||
|
||||
/** 按筛选条件汇总工单,统计范围不受当前分页影响。 */
|
||||
export async function getWorkOrderStatistics(input: ListInput = {}): Promise<WorkOrderStatistics> {
|
||||
const { whereClause, params } = buildWorkOrderWhere(input)
|
||||
const vipEvidencePendingWhere = `
|
||||
wo.status = 'accepted'
|
||||
AND wo.acceptance_json->>'reviewMode' = 'vip_auto'
|
||||
AND wo.acceptance_json->>'evidenceStatus' = 'pending'
|
||||
AND NULLIF(wo.acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
|
||||
`
|
||||
const statusExpression = input.vipEvidencePending
|
||||
? `CASE WHEN ${vipEvidencePendingWhere} THEN 'vip_evidence_pending' ELSE wo.status END`
|
||||
: 'wo.status'
|
||||
const paymentExpression = `
|
||||
CASE
|
||||
WHEN COALESCE(wo.material_json->'source'->>'orderAmountFen', '') ~ '^-?[0-9]+$'
|
||||
THEN (wo.material_json->'source'->>'orderAmountFen')::bigint
|
||||
ELSE COALESCE(o.total_amount, 0)
|
||||
END
|
||||
`
|
||||
const result = await query<WorkOrderStatisticsRow>(
|
||||
`
|
||||
SELECT
|
||||
${statusExpression} AS status,
|
||||
COUNT(*)::int AS total_count,
|
||||
COALESCE(SUM(${paymentExpression}), 0)::bigint AS user_payment_amount,
|
||||
COALESCE(SUM(wo.reward_amount), 0)::bigint AS reward_amount,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN o.pay_status = 'refunded' OR o.order_status = 'refunded'
|
||||
THEN ${paymentExpression}
|
||||
ELSE 0
|
||||
END
|
||||
), 0)::bigint AS refund_amount
|
||||
FROM work_orders wo
|
||||
LEFT JOIN orders o ON o.id = wo.order_id
|
||||
${whereClause}
|
||||
GROUP BY ${statusExpression}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
const statistics: WorkOrderStatistics = {
|
||||
totalCount: 0,
|
||||
userPaymentAmount: 0,
|
||||
rewardAmount: 0,
|
||||
refundAmount: 0,
|
||||
statusCounts: {},
|
||||
}
|
||||
for (const row of result.rows) {
|
||||
const count = Number(row.total_count || 0)
|
||||
statistics.totalCount += count
|
||||
statistics.userPaymentAmount += Number(row.user_payment_amount || 0)
|
||||
statistics.rewardAmount += Number(row.reward_amount || 0)
|
||||
statistics.refundAmount += Number(row.refund_amount || 0)
|
||||
statistics.statusCounts[row.status] = count
|
||||
}
|
||||
return statistics
|
||||
}
|
||||
|
||||
/** 汇总打手本人各订单状态的可结算金额,拼单按个人份额计算。 */
|
||||
export async function getWorkerOrderOverview(workerId: number | string): Promise<
|
||||
Array<{
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
getWorkerUserById,
|
||||
getWorkOrderStatistics,
|
||||
getWorkOrderByOrderItemId,
|
||||
getWorkProductRuleByKey,
|
||||
listAllWorkCategories,
|
||||
@@ -761,6 +762,13 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
workerSharingId: workerId,
|
||||
vipEvidencePending,
|
||||
})
|
||||
const statistics = await getWorkOrderStatistics({
|
||||
statuses,
|
||||
workOrderId,
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
workerSharingId: workerId,
|
||||
vipEvidencePending,
|
||||
})
|
||||
const shares = await listWorkOrderSharesByOrderIds(items.map((item) => Number(item.id)))
|
||||
const sharesByOrderId = new Map<number, WorkOrderShareRow[]>()
|
||||
for (const share of shares) {
|
||||
@@ -789,6 +797,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
}
|
||||
}),
|
||||
pagination: { page, pageSize, total },
|
||||
statistics,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,13 @@ import {
|
||||
updateAdminWorkOrder,
|
||||
updateAdminWorkOrderSharing,
|
||||
} from '@/services/admin'
|
||||
import type { CollectField, UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
||||
import type {
|
||||
CollectField,
|
||||
UploadedFile,
|
||||
WorkOrder,
|
||||
WorkOrderShare,
|
||||
WorkOrderStatistics,
|
||||
} from '@/types/worker-platform'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
@@ -959,6 +965,7 @@ export default function WorkOrdersPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<WorkOrderStatisticsBar statistics={ordersQuery.data?.data.statistics} />
|
||||
<Card
|
||||
title="接单工单"
|
||||
extra={
|
||||
@@ -1690,6 +1697,52 @@ export default function WorkOrdersPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
const WORK_ORDER_STATISTICS_STATUS_ORDER = [
|
||||
'pending_material',
|
||||
'unassigned',
|
||||
'open',
|
||||
'in_progress',
|
||||
'pending_acceptance',
|
||||
'vip_evidence_pending',
|
||||
'problem',
|
||||
'accepted',
|
||||
'cancelled',
|
||||
]
|
||||
|
||||
function WorkOrderStatisticsBar({ statistics }: { statistics?: WorkOrderStatistics }) {
|
||||
if (!statistics) return null
|
||||
const profit = Number(statistics.userPaymentAmount || 0) - Number(statistics.rewardAmount || 0)
|
||||
const statusItems = WORK_ORDER_STATISTICS_STATUS_ORDER.map((status) => ({
|
||||
status,
|
||||
count: Number(statistics.statusCounts?.[status] || 0),
|
||||
}))
|
||||
const otherStatusItems = Object.entries(statistics.statusCounts || {})
|
||||
.filter(([status]) => !WORK_ORDER_STATISTICS_STATUS_ORDER.includes(status))
|
||||
.map(([status, count]) => ({ status, count: Number(count || 0) }))
|
||||
|
||||
return (
|
||||
<Card size="small" bordered={false} className="worker-orders-statistics-card">
|
||||
<div className="worker-orders-statistics-summary">
|
||||
本次筛选统计共 <strong>{statistics.totalCount}</strong> 笔,预计总发单价{' '}
|
||||
<strong>{formatMoney(statistics.rewardAmount)}</strong>(不含销售提成),总接单价{' '}
|
||||
<strong>{formatMoney(statistics.userPaymentAmount)}</strong>,总退款{' '}
|
||||
<strong>{formatMoney(statistics.refundAmount)}</strong>,预计利润{' '}
|
||||
<strong className={profit < 0 ? 'is-negative' : ''}>{formatMoney(profit)}</strong>。
|
||||
</div>
|
||||
<div className="worker-orders-statistics-statuses">
|
||||
其中:
|
||||
{[...statusItems, ...otherStatusItems].map(({ status, count }, index) => (
|
||||
<span key={status}>
|
||||
{index > 0 ? ',' : ''}
|
||||
{formatStatus(status)}
|
||||
{count}笔
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function shouldSkipPublishConfirm() {
|
||||
try {
|
||||
return localStorage.getItem(SKIP_PUBLISH_CONFIRM_KEY) === '1'
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
WorkOrder,
|
||||
WorkOrderEvent,
|
||||
WorkOrderShare,
|
||||
WorkOrderStatistics,
|
||||
WorkProductRule,
|
||||
WorkerFinanceConfig,
|
||||
WorkerFinanceRequest,
|
||||
@@ -237,7 +238,11 @@ export function reviewAdminWorkerFinanceRequest(
|
||||
}
|
||||
|
||||
export function fetchAdminWorkOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<WorkerListResponse<WorkOrder>>('/api/v1/admin/worker-platform/orders', params)
|
||||
return apiGet<
|
||||
WorkerListResponse<WorkOrder> & {
|
||||
statistics: WorkOrderStatistics
|
||||
}
|
||||
>('/api/v1/admin/worker-platform/orders', params)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkOrderEvents(workOrderId: number) {
|
||||
|
||||
@@ -888,6 +888,34 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-card {
|
||||
border-left: 3px solid #ff6a00;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-card .ant-card-body {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-summary,
|
||||
.worker-orders-statistics-statuses {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-summary strong {
|
||||
color: #262626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-summary strong.is-negative {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.worker-orders-statistics-statuses {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.platform-section-gap {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@@ -325,6 +325,14 @@ export type WorkOrder = {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type WorkOrderStatistics = {
|
||||
totalCount: number
|
||||
userPaymentAmount: number
|
||||
rewardAmount: number
|
||||
refundAmount: number
|
||||
statusCounts: Record<string, number>
|
||||
}
|
||||
|
||||
/** 工单流转事件(订单详情中的操作日志) */
|
||||
export type WorkOrderEvent = {
|
||||
id: number
|
||||
|
||||
Reference in New Issue
Block a user