修复拼单明细并支持参与者撤单
This commit is contained in:
@@ -199,6 +199,16 @@ export function resolveWorkOrderShareJoinQuantity(input: {
|
|||||||
return input.takeRemaining ? Math.max(0, input.remaining) : input.quantity
|
return input.takeRemaining ? Math.max(0, input.remaining) : input.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 校验单个拼单份额能否撤单,并返回母工单应回退到的状态。 */
|
||||||
|
export function resolveWorkOrderShareCancellationStatus(
|
||||||
|
workOrderStatus: string,
|
||||||
|
shareStatus: string,
|
||||||
|
): 'open' | null {
|
||||||
|
if (!['open', 'pending_acceptance'].includes(workOrderStatus)) return null
|
||||||
|
if (!['joined', 'submitted'].includes(shareStatus)) return null
|
||||||
|
return 'open'
|
||||||
|
}
|
||||||
|
|
||||||
export async function joinWorkOrderShare(input: {
|
export async function joinWorkOrderShare(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
@@ -2038,7 +2048,9 @@ export async function acceptWorkOrderAndSettle(input: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (shares.length > 0) {
|
if (shares.length > 0) {
|
||||||
for (const share of shares) {
|
// 单独验收过的份额已经完成资金结算,统一验收时只处理尚未结算的份额。
|
||||||
|
const unsettledShares = shares.filter((share) => share.status !== 'accepted')
|
||||||
|
for (const share of unsettledShares) {
|
||||||
const shareWorkerId = Number(share.worker_id || 0)
|
const shareWorkerId = Number(share.worker_id || 0)
|
||||||
if (!shareWorkerId) continue
|
if (!shareWorkerId) continue
|
||||||
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
|
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
|
||||||
@@ -2263,7 +2275,10 @@ export async function acceptWorkOrderAndSettle(input: {
|
|||||||
eventType: 'accepted',
|
eventType: 'accepted',
|
||||||
fromStatus: 'pending_acceptance',
|
fromStatus: 'pending_acceptance',
|
||||||
toStatus: 'accepted',
|
toStatus: 'accepted',
|
||||||
payloadJson: JSON.stringify({ shareCount: shares.length }),
|
payloadJson: JSON.stringify({
|
||||||
|
shareCount: shares.length,
|
||||||
|
settledShareCount: shares.filter((share) => share.status !== 'accepted').length,
|
||||||
|
}),
|
||||||
now: input.now,
|
now: input.now,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2437,6 +2452,149 @@ export async function acceptWorkOrderShareAndSettle(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 后台撤销单个拼单份额,释放该打手押金并把份数重新放回大厅。 */
|
||||||
|
export async function cancelWorkOrderShare(input: {
|
||||||
|
workOrderId: number
|
||||||
|
shareId: number
|
||||||
|
now: string
|
||||||
|
reason?: string
|
||||||
|
actorName?: string
|
||||||
|
}): Promise<{
|
||||||
|
order: WorkOrderRow | null
|
||||||
|
share: WorkOrderShareRow | null
|
||||||
|
releasedDepositAmount: number
|
||||||
|
failureReason:
|
||||||
|
| 'work_order_not_cancellable'
|
||||||
|
| 'share_not_found'
|
||||||
|
| 'share_status_not_cancellable'
|
||||||
|
| null
|
||||||
|
}> {
|
||||||
|
return withTransaction(async (client) => {
|
||||||
|
const orderResult = await client.query<WorkOrderRow>(
|
||||||
|
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
|
||||||
|
[input.workOrderId],
|
||||||
|
)
|
||||||
|
const workOrder = orderResult.rows[0] || null
|
||||||
|
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) {
|
||||||
|
return {
|
||||||
|
order: workOrder,
|
||||||
|
share: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
failureReason: 'work_order_not_cancellable',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareResult = await client.query<WorkOrderShareRow>(
|
||||||
|
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||||
|
[input.shareId, input.workOrderId],
|
||||||
|
)
|
||||||
|
const share = shareResult.rows[0] || null
|
||||||
|
if (!share) {
|
||||||
|
return {
|
||||||
|
order: workOrder,
|
||||||
|
share: null,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
failureReason: 'share_not_found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextOrderStatus = resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
|
||||||
|
if (!nextOrderStatus) {
|
||||||
|
return {
|
||||||
|
order: workOrder,
|
||||||
|
share,
|
||||||
|
releasedDepositAmount: 0,
|
||||||
|
failureReason: 'share_status_not_cancellable',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerId = Number(share.worker_id || 0)
|
||||||
|
let releasedDepositAmount = 0
|
||||||
|
if (workerId > 0) {
|
||||||
|
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||||
|
releasedDepositAmount = Math.min(
|
||||||
|
Number(share.share_deposit || 0),
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
)
|
||||||
|
if (releasedDepositAmount > 0) {
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
|
||||||
|
const nextFrozen = Math.max(
|
||||||
|
0,
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_wallets
|
||||||
|
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||||
|
WHERE worker_id = $4`,
|
||||||
|
[nextAvailable, nextFrozen, input.now, workerId],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO worker_wallet_ledgers (
|
||||||
|
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||||
|
related_work_order_id, note, payload_json, created_at
|
||||||
|
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '后台撤销拼单份额退还押金', $6::jsonb, $7)`,
|
||||||
|
[
|
||||||
|
workerId,
|
||||||
|
releasedDepositAmount,
|
||||||
|
nextAvailable,
|
||||||
|
nextFrozen,
|
||||||
|
input.workOrderId,
|
||||||
|
JSON.stringify({
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
shareId: input.shareId,
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
}),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE work_order_shares
|
||||||
|
SET status = 'cancelled', updated_at = $1
|
||||||
|
WHERE id = $2`,
|
||||||
|
[input.now, input.shareId],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`UPDATE work_orders
|
||||||
|
SET status = $1,
|
||||||
|
submitted_at = NULL,
|
||||||
|
published_at = COALESCE(published_at, $2),
|
||||||
|
updated_at = $2
|
||||||
|
WHERE id = $3`,
|
||||||
|
[nextOrderStatus, input.now, input.workOrderId],
|
||||||
|
)
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: 'sharing_share_cancelled_by_admin',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: nextOrderStatus,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
shareId: input.shareId,
|
||||||
|
workerId,
|
||||||
|
quantity: Number(share.quantity || 0),
|
||||||
|
releasedDepositAmount,
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedShareResult = await client.query<WorkOrderShareRow>(
|
||||||
|
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||||
|
[input.shareId],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||||
|
share: updatedShareResult.rows[0] || null,
|
||||||
|
releasedDepositAmount,
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveProblemWorkOrder(input: {
|
export async function resolveProblemWorkOrder(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
action: ProblemWorkOrderResolutionAction
|
action: ProblemWorkOrderResolutionAction
|
||||||
@@ -3230,7 +3388,16 @@ function buildWorkOrderWhere({
|
|||||||
}
|
}
|
||||||
if (workerId) {
|
if (workerId) {
|
||||||
params.push(workerId)
|
params.push(workerId)
|
||||||
filters.push(`wo.assigned_worker_id = $${params.length}`)
|
filters.push(`(
|
||||||
|
wo.assigned_worker_id = $${params.length}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM work_order_shares wos
|
||||||
|
WHERE wos.work_order_id = wo.id
|
||||||
|
AND wos.worker_id = $${params.length}
|
||||||
|
AND wos.status != 'cancelled'
|
||||||
|
)
|
||||||
|
)`)
|
||||||
}
|
}
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
params.push(categoryId)
|
params.push(categoryId)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
acceptAdminWorkOrders,
|
acceptAdminWorkOrders,
|
||||||
assignAdminWorkOrderToWorker,
|
assignAdminWorkOrderToWorker,
|
||||||
cancelAdminWorkOrder,
|
cancelAdminWorkOrder,
|
||||||
|
cancelAdminWorkOrderShare,
|
||||||
unassignAdminWorkOrder,
|
unassignAdminWorkOrder,
|
||||||
creditAdminWorkerWallet,
|
creditAdminWorkerWallet,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
@@ -854,6 +855,31 @@ router.post(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/orders/:workOrderId/shares/:shareId/cancel',
|
||||||
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
cancelAdminWorkOrderShare(
|
||||||
|
String(req.params.workOrderId || ''),
|
||||||
|
String(req.params.shareId || ''),
|
||||||
|
req.body || {},
|
||||||
|
req.adminSession?.username || '',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '拼单参与者已撤单,份数已退回大厅',
|
||||||
|
errorMessage: '撤销拼单参与者失败',
|
||||||
|
scope: '[admin/worker-platform/orders/:workOrderId/shares/:shareId/cancel]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'work_order_share_cancelled',
|
||||||
|
targetType: 'work_order_share',
|
||||||
|
targetId: String(req.params.shareId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/deduct-pending-deposit',
|
'/worker-platform/orders/:workOrderId/deduct-pending-deposit',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
|||||||
@@ -310,11 +310,13 @@ async function publishWorkOrderSnapshots(input: {
|
|||||||
const viewedAt =
|
const viewedAt =
|
||||||
views.find((item) => Number(item.work_order_id) === workOrderId)?.viewed_at || null
|
views.find((item) => Number(item.work_order_id) === workOrderId)?.viewed_at || null
|
||||||
const share = shares.find((item) => Number(item.worker_id) === workerId)
|
const share = shares.find((item) => Number(item.worker_id) === workerId)
|
||||||
|
const isAssignedWorker = Number(workOrder.assigned_worker_id || 0) === workerId
|
||||||
|
const workerOrderVisible = isAssignedWorker || Boolean(share && share.status !== 'cancelled')
|
||||||
publishRealtimeEvent({
|
publishRealtimeEvent({
|
||||||
type: 'work_order.changed',
|
type: 'work_order.changed',
|
||||||
entityId: workOrderId,
|
entityId: workOrderId,
|
||||||
scopes: ['worker_my_orders'],
|
scopes: ['worker_my_orders'],
|
||||||
operation: 'upsert',
|
operation: workerOrderVisible ? 'upsert' : 'remove',
|
||||||
data: mapWorkOrderForWorker(
|
data: mapWorkOrderForWorker(
|
||||||
workOrder,
|
workOrder,
|
||||||
resolveWorkerPermissions(worker),
|
resolveWorkerPermissions(worker),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
addWorkerWalletCredit,
|
addWorkerWalletCredit,
|
||||||
assignWorkOrderToWorker,
|
assignWorkOrderToWorker,
|
||||||
cancelAssignedWorkOrder,
|
cancelAssignedWorkOrder,
|
||||||
|
cancelWorkOrderShare,
|
||||||
returnAssignedWorkOrderToHall,
|
returnAssignedWorkOrderToHall,
|
||||||
unassignWorkOrder,
|
unassignWorkOrder,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
@@ -2040,6 +2041,52 @@ export async function acceptAdminWorkOrderShare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancelAdminWorkOrderShare(
|
||||||
|
workOrderId: number | string,
|
||||||
|
shareId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
const result = await cancelWorkOrderShare({
|
||||||
|
workOrderId: Number(workOrderId),
|
||||||
|
shareId: Number(shareId),
|
||||||
|
now: nowIso(),
|
||||||
|
reason: String(payload.reason || '').trim(),
|
||||||
|
actorName,
|
||||||
|
})
|
||||||
|
if (result.failureReason === 'work_order_not_cancellable') {
|
||||||
|
throw createHttpError('当前拼单状态不能撤销参与者', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_cancel_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'share_not_found') {
|
||||||
|
throw createHttpError('未找到该拼单参与记录', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'work_order_sharing_share_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'share_status_not_cancellable' || !result.share || !result.order) {
|
||||||
|
throw createHttpError('只有已参与或已提交验收的拼单份额可以撤单', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_share_cancel_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerId = Number(result.share.worker_id || 0)
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrderId),
|
||||||
|
workerIds: await resolveWorkOrderRealtimeWorkerIds(result.order),
|
||||||
|
hallChanged: true,
|
||||||
|
})
|
||||||
|
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderAdmin(result.order),
|
||||||
|
share: mapWorkOrderShare(result.share),
|
||||||
|
releasedDepositAmount: result.releasedDepositAmount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
|
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
|
||||||
const workOrderIds = [
|
const workOrderIds = [
|
||||||
...new Set(
|
...new Set(
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import type {
|
|||||||
WorkProductRuleRow,
|
WorkProductRuleRow,
|
||||||
WorkerUserRow,
|
WorkerUserRow,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import { resolveWorkOrderShareJoinQuantity } from '../../repositories/worker-platform/index.js'
|
import {
|
||||||
|
resolveWorkOrderShareCancellationStatus,
|
||||||
|
resolveWorkOrderShareJoinQuantity,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||||
import {
|
import {
|
||||||
assertWithdrawChannelAllowedForAmount,
|
assertWithdrawChannelAllowedForAmount,
|
||||||
@@ -729,6 +732,15 @@ test('后台拼单聚合状态同时收敛筛选语义和操作能力', () => {
|
|||||||
assert.equal(filled.capabilities.canUnpublish, false)
|
assert.equal(filled.capabilities.canUnpublish, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('拼单份额撤单只允许未验收份额并让母工单回到大厅状态', () => {
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('open', 'joined'), 'open')
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('open', 'submitted'), 'open')
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('pending_acceptance', 'submitted'), 'open')
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('pending_acceptance', 'accepted'), null)
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('accepted', 'submitted'), null)
|
||||||
|
assert.equal(resolveWorkOrderShareCancellationStatus('open', 'cancelled'), null)
|
||||||
|
})
|
||||||
|
|
||||||
test('resolveCollectSubmitTargetWorkOrder returns the only pending work order when no id is provided', () => {
|
test('resolveCollectSubmitTargetWorkOrder returns the only pending work order when no id is provided', () => {
|
||||||
const workOrder = buildWorkOrderRow()
|
const workOrder = buildWorkOrderRow()
|
||||||
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', [workOrder], '')
|
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', [workOrder], '')
|
||||||
|
|||||||
@@ -182,6 +182,15 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
|
|||||||
return { title: '更改了订单信息', color: 'default' }
|
return { title: '更改了订单信息', color: 'default' }
|
||||||
case 'sharing_config_updated':
|
case 'sharing_config_updated':
|
||||||
return { title: '更新了拼单配置', color: 'default' }
|
return { title: '更新了拼单配置', color: 'default' }
|
||||||
|
case 'sharing_share_cancelled_by_admin': {
|
||||||
|
const workerId = readPayload(event, 'workerId')
|
||||||
|
const quantity = readPayload(event, 'quantity')
|
||||||
|
return {
|
||||||
|
title: `撤销拼单参与者${workerId ? `(打手 #${workerId})` : ''}`,
|
||||||
|
description: `${quantity ? `撤销 ${quantity} 份;` : ''}${note ? `原因:${note}` : '份数已退回接单大厅'}`,
|
||||||
|
color: 'red',
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'pending_deposit_deducted':
|
case 'pending_deposit_deducted':
|
||||||
return { title: '扣减了待解冻押金', color: 'default' }
|
return { title: '扣减了待解冻押金', color: 'default' }
|
||||||
case 'voucher_backfilled':
|
case 'voucher_backfilled':
|
||||||
|
|||||||
@@ -349,13 +349,17 @@ function applyAdminRealtimeEvent(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (event.type === 'work_order.changed' && event.scopes.includes('admin_work_orders')) {
|
||||||
event.type === 'work_order.changed' &&
|
// 拼单明细是独立查询,工单列表快照更新时也要同步刷新当前打开的明细。
|
||||||
event.scopes.includes('admin_work_orders') &&
|
void queryClient.invalidateQueries({
|
||||||
isRecord(event.data)
|
queryKey: ['admin-worker-platform-order-sharing', event.entityId],
|
||||||
) {
|
})
|
||||||
const data = event.data
|
if (isRecord(event.data)) {
|
||||||
patchAdminWorkOrderCaches(queryClient, { ...event, data })
|
const data = event.data
|
||||||
|
patchAdminWorkOrderCaches(queryClient, { ...event, data })
|
||||||
|
} else {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
acceptAdminWorkOrders,
|
acceptAdminWorkOrders,
|
||||||
assignAdminWorkOrderToWorker,
|
assignAdminWorkOrderToWorker,
|
||||||
cancelAdminWorkOrder,
|
cancelAdminWorkOrder,
|
||||||
|
cancelAdminWorkOrderShare,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
deductAdminWorkOrderPendingDeposit,
|
deductAdminWorkOrderPendingDeposit,
|
||||||
deleteAdminWorkOrder,
|
deleteAdminWorkOrder,
|
||||||
@@ -120,6 +121,10 @@ type CancelOrderFormValues = {
|
|||||||
reason?: string
|
reason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CancelShareFormValues = {
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function WorkOrdersPanel() {
|
export default function WorkOrdersPanel() {
|
||||||
const { message, modal } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -142,6 +147,7 @@ export default function WorkOrdersPanel() {
|
|||||||
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
||||||
const [materialPasteText, setMaterialPasteText] = useState('')
|
const [materialPasteText, setMaterialPasteText] = useState('')
|
||||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||||
|
const [cancelShare, setCancelShare] = useState<WorkOrderShare | null>(null)
|
||||||
const [assignOrder, setAssignOrder] = useState<WorkOrder | null>(null)
|
const [assignOrder, setAssignOrder] = useState<WorkOrder | null>(null)
|
||||||
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
const [assignWorkerId, setAssignWorkerId] = useState<number | undefined>(undefined)
|
||||||
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
||||||
@@ -151,6 +157,7 @@ export default function WorkOrdersPanel() {
|
|||||||
const [resolutionForm] = Form.useForm()
|
const [resolutionForm] = Form.useForm()
|
||||||
const [materialForm] = Form.useForm<MaterialFormValues>()
|
const [materialForm] = Form.useForm<MaterialFormValues>()
|
||||||
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
|
const [cancelForm] = Form.useForm<CancelOrderFormValues>()
|
||||||
|
const [cancelShareForm] = Form.useForm<CancelShareFormValues>()
|
||||||
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
||||||
const [editForm] = Form.useForm<{
|
const [editForm] = Form.useForm<{
|
||||||
productName?: string
|
productName?: string
|
||||||
@@ -349,6 +356,24 @@ export default function WorkOrdersPanel() {
|
|||||||
sharingForm.resetFields()
|
sharingForm.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitCancelShare(values: CancelShareFormValues) {
|
||||||
|
if (!sharingOrder || !cancelShare) return
|
||||||
|
const workOrderId = sharingOrder.workOrderId
|
||||||
|
const succeeded = await runAction(
|
||||||
|
() =>
|
||||||
|
cancelAdminWorkOrderShare(workOrderId, cancelShare.shareId, {
|
||||||
|
reason: String(values.reason || '').trim(),
|
||||||
|
}),
|
||||||
|
'该参与者已撤单,押金和拼单份数已恢复',
|
||||||
|
)
|
||||||
|
if (!succeeded) return
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin-worker-platform-order-sharing', workOrderId],
|
||||||
|
})
|
||||||
|
setCancelShare(null)
|
||||||
|
cancelShareForm.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -816,7 +841,9 @@ export default function WorkOrdersPanel() {
|
|||||||
) : null}
|
) : null}
|
||||||
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
||||||
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
||||||
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing ? '拼单明细' : '拼单配置'}
|
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
||||||
|
? '拼单明细'
|
||||||
|
: '拼单配置'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canAssign ? (
|
{row.capabilities?.canAssign ? (
|
||||||
@@ -1589,7 +1616,9 @@ export default function WorkOrdersPanel() {
|
|||||||
open={Boolean(sharingOrder)}
|
open={Boolean(sharingOrder)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setSharingOrder(null)
|
setSharingOrder(null)
|
||||||
|
setCancelShare(null)
|
||||||
sharingForm.resetFields()
|
sharingForm.resetFields()
|
||||||
|
cancelShareForm.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => sharingForm.submit()}
|
onOk={() => sharingForm.submit()}
|
||||||
footer={sharingOrder?.capabilities?.canConfigureSharing ? undefined : null}
|
footer={sharingOrder?.capabilities?.canConfigureSharing ? undefined : null}
|
||||||
@@ -1679,6 +1708,7 @@ export default function WorkOrdersPanel() {
|
|||||||
rowKey="shareId"
|
rowKey="shareId"
|
||||||
size="small"
|
size="small"
|
||||||
dataSource={sharingQuery.data?.data.shares || []}
|
dataSource={sharingQuery.data?.data.shares || []}
|
||||||
|
loading={sharingQuery.isFetching}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
@@ -1709,8 +1739,14 @@ export default function WorkOrdersPanel() {
|
|||||||
width: 150,
|
width: 150,
|
||||||
render: (_, share) => (
|
render: (_, share) => (
|
||||||
<ImagePreviewList
|
<ImagePreviewList
|
||||||
files={Array.isArray(share.acceptance?.files) ? share.acceptance.files : []}
|
files={
|
||||||
imageUrls={Array.isArray(share.acceptance?.imageUrls) ? share.acceptance.imageUrls : []}
|
Array.isArray(share.acceptance?.files) ? share.acceptance.files : []
|
||||||
|
}
|
||||||
|
imageUrls={
|
||||||
|
Array.isArray(share.acceptance?.imageUrls)
|
||||||
|
? share.acceptance.imageUrls
|
||||||
|
: []
|
||||||
|
}
|
||||||
size={44}
|
size={44}
|
||||||
maxVisible={2}
|
maxVisible={2}
|
||||||
/>
|
/>
|
||||||
@@ -1722,35 +1758,55 @@ export default function WorkOrdersPanel() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 110,
|
width: 190,
|
||||||
render: (_, share) =>
|
render: (_, share) => {
|
||||||
share.status === 'submitted' ? (
|
if (share.status === 'accepted') return <Tag color="green">已验收</Tag>
|
||||||
<Button
|
if (share.status === 'cancelled') return <Tag>已撤单</Tag>
|
||||||
type="primary"
|
return (
|
||||||
size="small"
|
<Space size={6}>
|
||||||
onClick={() => {
|
{share.status === 'submitted' ? (
|
||||||
void runAction(
|
<Button
|
||||||
() => acceptAdminWorkOrderShare(sharingOrder.workOrderId, share.shareId),
|
type="primary"
|
||||||
'该拼单参与者已验收',
|
size="small"
|
||||||
).then((succeeded) => {
|
onClick={() => {
|
||||||
if (succeeded) {
|
void runAction(
|
||||||
void queryClient.invalidateQueries({
|
() =>
|
||||||
queryKey: [
|
acceptAdminWorkOrderShare(
|
||||||
'admin-worker-platform-order-sharing',
|
sharingOrder.workOrderId,
|
||||||
sharingOrder.workOrderId,
|
share.shareId,
|
||||||
],
|
),
|
||||||
|
'该拼单参与者已验收',
|
||||||
|
).then((succeeded) => {
|
||||||
|
if (succeeded) {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
'admin-worker-platform-order-sharing',
|
||||||
|
sharingOrder.workOrderId,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
})
|
>
|
||||||
}}
|
单独验收
|
||||||
>
|
</Button>
|
||||||
单独验收
|
) : (
|
||||||
</Button>
|
<Typography.Text type="secondary">待提交</Typography.Text>
|
||||||
) : share.status === 'accepted' ? (
|
)}
|
||||||
<Tag color="green">已验收</Tag>
|
<Button
|
||||||
) : (
|
danger
|
||||||
<Typography.Text type="secondary">待提交</Typography.Text>
|
size="small"
|
||||||
),
|
icon={<UndoOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setCancelShare(share)
|
||||||
|
cancelShareForm.setFieldsValue({ reason: '' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
撤单
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -1759,6 +1815,37 @@ export default function WorkOrdersPanel() {
|
|||||||
</Space>
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`撤销拼单参与者 · ${
|
||||||
|
cancelShare?.worker?.displayName || cancelShare?.worker?.username || ''
|
||||||
|
}`}
|
||||||
|
open={Boolean(cancelShare)}
|
||||||
|
okText="确认撤单"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
onCancel={() => {
|
||||||
|
setCancelShare(null)
|
||||||
|
cancelShareForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => cancelShareForm.submit()}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
{cancelShare ? (
|
||||||
|
<Form form={cancelShareForm} layout="vertical" onFinish={submitCancelShare}>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message={`将撤销该打手承接的 ${cancelShare.quantity} 份拼单`}
|
||||||
|
description="撤单后会退还该份额冻结的押金,空出的份数重新回到接单大厅;已提交的验收资料会保留在后台记录中。"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Form.Item label="撤单原因" name="reason">
|
||||||
|
<Input.TextArea rows={3} placeholder="选填,用于后台流转记录" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,6 +470,18 @@ export function acceptAdminWorkOrderShare(workOrderId: number, shareId: number)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cancelAdminWorkOrderShare(
|
||||||
|
workOrderId: number,
|
||||||
|
shareId: number,
|
||||||
|
payload: { reason?: string },
|
||||||
|
) {
|
||||||
|
return apiPost<{
|
||||||
|
order: WorkOrder
|
||||||
|
share: WorkOrderShare
|
||||||
|
releasedDepositAmount: number
|
||||||
|
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/shares/${shareId}/cancel`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
export function acceptAdminWorkOrders(workOrderIds: number[]) {
|
export function acceptAdminWorkOrders(workOrderIds: number[]) {
|
||||||
return apiPost<{ orders: WorkOrder[] }>('/api/v1/admin/worker-platform/orders/accept-batch', {
|
return apiPost<{ orders: WorkOrder[] }>('/api/v1/admin/worker-platform/orders/accept-batch', {
|
||||||
workOrderIds,
|
workOrderIds,
|
||||||
|
|||||||
Reference in New Issue
Block a user