打手端待审撤单与未处理反馈订单归入问题单分组并标注来源角标
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- 打手查看工单资料更新的时间:客服更新资料后订单展示"资料已更新"角标,打手打开详情后清除。
|
||||||
|
ALTER TABLE worker_work_order_views
|
||||||
|
ADD COLUMN IF NOT EXISTS material_seen_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_work_order_views.material_seen_at IS '打手最近一次打开工单详情的时间,用于计算资料已更新角标';
|
||||||
@@ -2,6 +2,24 @@ import type { PoolClient } from 'pg'
|
|||||||
|
|
||||||
import { query } from '../../db/client.js'
|
import { query } from '../../db/client.js'
|
||||||
|
|
||||||
|
/** 批量查询工单最近一次客服更新资料的时间,用于打手端"资料已更新"角标。 */
|
||||||
|
export async function getLatestMaterialEventAtByOrderIds(
|
||||||
|
workOrderIds: number[],
|
||||||
|
): Promise<Map<number, string>> {
|
||||||
|
if (workOrderIds.length === 0) return new Map()
|
||||||
|
const result = await query<{ work_order_id: number; created_at: string }>(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT ON (work_order_id) work_order_id, created_at
|
||||||
|
FROM work_order_events
|
||||||
|
WHERE work_order_id = ANY($1::bigint[])
|
||||||
|
AND event_type IN ('material_updated', 'material_completed')
|
||||||
|
ORDER BY work_order_id, id DESC
|
||||||
|
`,
|
||||||
|
[workOrderIds],
|
||||||
|
)
|
||||||
|
return new Map(result.rows.map((row) => [Number(row.work_order_id), String(row.created_at)]))
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkOrderEventRow = {
|
export type WorkOrderEventRow = {
|
||||||
id: number
|
id: number
|
||||||
work_order_id: number
|
work_order_id: number
|
||||||
|
|||||||
@@ -122,9 +122,13 @@ function workOrderSearchExpression(alias = 'wo') {
|
|||||||
)`
|
)`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打手订单筛选使用个人份额状态,而不是拼单母工单的 open 状态。 */
|
/**
|
||||||
function workerOrderStatusExpression(workerShareAlias = 'worker_share') {
|
* 打手订单个人状态:拼单按个人份额状态计算。
|
||||||
return `CASE
|
* 传入 workerId 时,代练中但存在待审核取消申请或未处理反馈的订单归入问题单,
|
||||||
|
* 客服驳回/解决后自动回到代练中(计算型归类,无状态迁移)。
|
||||||
|
*/
|
||||||
|
export function workerOrderStatusExpression(workerId = 0, workerShareAlias = 'worker_share') {
|
||||||
|
const baseExpression = `CASE
|
||||||
WHEN wo.status IN ('accepted', 'cancelled', 'problem') THEN wo.status
|
WHEN wo.status IN ('accepted', 'cancelled', 'problem') THEN wo.status
|
||||||
ELSE COALESCE(CASE ${workerShareAlias}.status
|
ELSE COALESCE(CASE ${workerShareAlias}.status
|
||||||
WHEN 'joined' THEN 'in_progress'
|
WHEN 'joined' THEN 'in_progress'
|
||||||
@@ -134,6 +138,27 @@ function workerOrderStatusExpression(workerShareAlias = 'worker_share') {
|
|||||||
ELSE wo.status
|
ELSE wo.status
|
||||||
END, wo.status)
|
END, wo.status)
|
||||||
END`
|
END`
|
||||||
|
const normalizedWorkerId = Math.max(0, Math.floor(Number(workerId) || 0))
|
||||||
|
if (normalizedWorkerId <= 0) return baseExpression
|
||||||
|
const pendingHandlingExists = `(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM worker_order_cancel_requests wcr
|
||||||
|
WHERE wcr.work_order_id = wo.id
|
||||||
|
AND wcr.worker_id = ${normalizedWorkerId}
|
||||||
|
AND wcr.status = 'pending'
|
||||||
|
) OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM worker_order_feedbacks wf
|
||||||
|
WHERE wf.work_order_id = wo.id
|
||||||
|
AND wf.worker_id = ${normalizedWorkerId}
|
||||||
|
AND wf.status = 'pending'
|
||||||
|
)
|
||||||
|
)`
|
||||||
|
return `CASE
|
||||||
|
WHEN (${baseExpression}) = 'in_progress' AND ${pendingHandlingExists} THEN 'problem'
|
||||||
|
ELSE ${baseExpression}
|
||||||
|
END`
|
||||||
}
|
}
|
||||||
export async function getWorkOrderByOrderItemId(
|
export async function getWorkOrderByOrderItemId(
|
||||||
orderItemId: number | string,
|
orderItemId: number | string,
|
||||||
@@ -343,15 +368,44 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise
|
|||||||
WITH worker_orders AS (
|
WITH worker_orders AS (
|
||||||
SELECT
|
SELECT
|
||||||
CASE
|
CASE
|
||||||
WHEN wos.id IS NOT NULL AND wo.status NOT IN ('accepted', 'cancelled', 'problem')
|
WHEN (
|
||||||
THEN CASE wos.status
|
CASE
|
||||||
WHEN 'joined' THEN 'in_progress'
|
WHEN wos.id IS NOT NULL AND wo.status NOT IN ('accepted', 'cancelled', 'problem')
|
||||||
WHEN 'submitted' THEN 'pending_acceptance'
|
THEN CASE wos.status
|
||||||
WHEN 'accepted' THEN 'accepted'
|
WHEN 'joined' THEN 'in_progress'
|
||||||
WHEN 'cancelled' THEN 'cancelled'
|
WHEN 'submitted' THEN 'pending_acceptance'
|
||||||
|
WHEN 'accepted' THEN 'accepted'
|
||||||
|
WHEN 'cancelled' THEN 'cancelled'
|
||||||
|
ELSE wo.status
|
||||||
|
END
|
||||||
ELSE wo.status
|
ELSE wo.status
|
||||||
END
|
END
|
||||||
ELSE wo.status
|
) = 'in_progress' AND (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM worker_order_cancel_requests wcr
|
||||||
|
WHERE wcr.work_order_id = wo.id
|
||||||
|
AND wcr.worker_id = $1
|
||||||
|
AND wcr.status = 'pending'
|
||||||
|
) OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM worker_order_feedbacks wf
|
||||||
|
WHERE wf.work_order_id = wo.id
|
||||||
|
AND wf.worker_id = $1
|
||||||
|
AND wf.status = 'pending'
|
||||||
|
)
|
||||||
|
) THEN 'problem'
|
||||||
|
ELSE CASE
|
||||||
|
WHEN wos.id IS NOT NULL AND wo.status NOT IN ('accepted', 'cancelled', 'problem')
|
||||||
|
THEN CASE wos.status
|
||||||
|
WHEN 'joined' THEN 'in_progress'
|
||||||
|
WHEN 'submitted' THEN 'pending_acceptance'
|
||||||
|
WHEN 'accepted' THEN 'accepted'
|
||||||
|
WHEN 'cancelled' THEN 'cancelled'
|
||||||
|
ELSE wo.status
|
||||||
|
END
|
||||||
|
ELSE wo.status
|
||||||
|
END
|
||||||
END AS status,
|
END AS status,
|
||||||
wo.acceptance_json,
|
wo.acceptance_json,
|
||||||
wos.id IS NOT NULL AS is_worker_share,
|
wos.id IS NOT NULL AS is_worker_share,
|
||||||
@@ -678,7 +732,9 @@ export function buildWorkOrderWhere({
|
|||||||
params.push(normalizedStatuses)
|
params.push(normalizedStatuses)
|
||||||
const statusesPlaceholder = `$${params.length}`
|
const statusesPlaceholder = `$${params.length}`
|
||||||
if (workerSharingId) {
|
if (workerSharingId) {
|
||||||
filters.push(`${workerOrderStatusExpression()} = ANY(${statusesPlaceholder}::text[])`)
|
filters.push(
|
||||||
|
`${workerOrderStatusExpression(workerSharingId)} = ANY(${statusesPlaceholder}::text[])`,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
filters.push(
|
filters.push(
|
||||||
`${useOperationalStatus ? adminOperationalStatusExpression('wo', 'operational_share_summary') : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`,
|
`${useOperationalStatus ? adminOperationalStatusExpression('wo', 'operational_share_summary') : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`,
|
||||||
|
|||||||
@@ -201,6 +201,23 @@ async function applyWorkerCancelRequestPenaltyWithClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 打手提交撤单申请;待审核期间订单与押金均保持原状。 */
|
/** 打手提交撤单申请;待审核期间订单与押金均保持原状。 */
|
||||||
|
/** 批量查询打手在各工单上待审核的撤单申请,用于订单角标与问题单归类。 */
|
||||||
|
export async function getPendingCancelRequestOrderIds(
|
||||||
|
workerId: number,
|
||||||
|
workOrderIds: number[],
|
||||||
|
): Promise<Set<number>> {
|
||||||
|
if (workOrderIds.length === 0) return new Set()
|
||||||
|
const result = await query<{ work_order_id: number }>(
|
||||||
|
`
|
||||||
|
SELECT work_order_id
|
||||||
|
FROM worker_order_cancel_requests
|
||||||
|
WHERE worker_id = $1 AND work_order_id = ANY($2::bigint[]) AND status = 'pending'
|
||||||
|
`,
|
||||||
|
[Number(workerId), workOrderIds],
|
||||||
|
)
|
||||||
|
return new Set(result.rows.map((row) => Number(row.work_order_id)))
|
||||||
|
}
|
||||||
|
|
||||||
export async function createWorkerCancelRequest(input: {
|
export async function createWorkerCancelRequest(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
@@ -39,6 +39,23 @@ async function getWorkerOrderFeedbackByIdWithClient(
|
|||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量查询打手在各工单上未处理的问题反馈,用于订单角标与问题单归类。 */
|
||||||
|
export async function getPendingFeedbackOrderIds(
|
||||||
|
workerId: number,
|
||||||
|
workOrderIds: number[],
|
||||||
|
): Promise<Set<number>> {
|
||||||
|
if (workOrderIds.length === 0) return new Set()
|
||||||
|
const result = await query<{ work_order_id: number }>(
|
||||||
|
`
|
||||||
|
SELECT work_order_id
|
||||||
|
FROM worker_order_feedbacks
|
||||||
|
WHERE worker_id = $1 AND work_order_id = ANY($2::bigint[]) AND status = 'pending'
|
||||||
|
`,
|
||||||
|
[Number(workerId), workOrderIds],
|
||||||
|
)
|
||||||
|
return new Set(result.rows.map((row) => Number(row.work_order_id)))
|
||||||
|
}
|
||||||
|
|
||||||
export async function createWorkerOrderFeedback(input: {
|
export async function createWorkerOrderFeedback(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
@@ -36,15 +36,19 @@ export async function upsertWorkerWorkOrderNote(input: {
|
|||||||
return String(result.rows[0]?.note || '')
|
return String(result.rows[0]?.note || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询打手已查看过的工单,用于计算"新"标识。 */
|
/** 查询打手已查看过的工单,用于计算"新"标识与资料已更新角标。 */
|
||||||
export async function listWorkerWorkOrderViews(
|
export async function listWorkerWorkOrderViews(
|
||||||
workerId: number | string,
|
workerId: number | string,
|
||||||
workOrderIds: number[],
|
workOrderIds: number[],
|
||||||
): Promise<Array<{ work_order_id: number; viewed_at: string }>> {
|
): Promise<Array<{ work_order_id: number; viewed_at: string; material_seen_at: string | null }>> {
|
||||||
if (workOrderIds.length === 0) return []
|
if (workOrderIds.length === 0) return []
|
||||||
const result = await query<{ work_order_id: number; viewed_at: string }>(
|
const result = await query<{
|
||||||
|
work_order_id: number
|
||||||
|
viewed_at: string
|
||||||
|
material_seen_at: string | null
|
||||||
|
}>(
|
||||||
`
|
`
|
||||||
SELECT work_order_id, viewed_at
|
SELECT work_order_id, viewed_at, material_seen_at
|
||||||
FROM worker_work_order_views
|
FROM worker_work_order_views
|
||||||
WHERE worker_id = $1 AND work_order_id = ANY($2::bigint[])
|
WHERE worker_id = $1 AND work_order_id = ANY($2::bigint[])
|
||||||
`,
|
`,
|
||||||
@@ -53,12 +57,27 @@ export async function listWorkerWorkOrderViews(
|
|||||||
return result.rows
|
return result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 首次查看工单时写入已读记录;重复调用保持原查看时间不变。 */
|
/**
|
||||||
|
* 首次查看工单时写入已读记录,重复调用保持原查看时间不变;
|
||||||
|
* materialSeen 为 true 时刷新资料查看时间,用于清除"资料已更新"角标。
|
||||||
|
*/
|
||||||
export async function markWorkerWorkOrderViewed(input: {
|
export async function markWorkerWorkOrderViewed(input: {
|
||||||
workOrderId: number
|
workOrderId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
now: string
|
now: string
|
||||||
|
materialSeen?: boolean
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
|
if (input.materialSeen) {
|
||||||
|
await query(
|
||||||
|
`
|
||||||
|
INSERT INTO worker_work_order_views (work_order_id, worker_id, viewed_at, material_seen_at)
|
||||||
|
VALUES ($1, $2, $3, $3)
|
||||||
|
ON CONFLICT (work_order_id, worker_id) DO UPDATE
|
||||||
|
SET material_seen_at = EXCLUDED.material_seen_at
|
||||||
|
`,
|
||||||
|
[input.workOrderId, input.workerId, input.now],
|
||||||
|
)
|
||||||
|
}
|
||||||
const inserted = await query<{ viewed_at: string }>(
|
const inserted = await query<{ viewed_at: string }>(
|
||||||
`
|
`
|
||||||
INSERT INTO worker_work_order_views (work_order_id, worker_id, viewed_at)
|
INSERT INTO worker_work_order_views (work_order_id, worker_id, viewed_at)
|
||||||
|
|||||||
@@ -157,7 +157,11 @@ router.post(
|
|||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
createRouteHandler(
|
createRouteHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
markWorkerOrderRead(String(req.params.workOrderId || ''), getRequiredWorkerSession(req)),
|
markWorkerOrderRead(
|
||||||
|
String(req.params.workOrderId || ''),
|
||||||
|
req.body || {},
|
||||||
|
getRequiredWorkerSession(req),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
successMessage: '订单已标记为已读',
|
successMessage: '订单已标记为已读',
|
||||||
errorMessage: '标记订单已读失败',
|
errorMessage: '标记订单已读失败',
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { runtimeConfig } from '../../config/runtime.js'
|
|||||||
import {
|
import {
|
||||||
createWorkerCancelRequest,
|
createWorkerCancelRequest,
|
||||||
createWorkerOrderFeedback,
|
createWorkerOrderFeedback,
|
||||||
|
getLatestMaterialEventAtByOrderIds,
|
||||||
|
getPendingCancelRequestOrderIds,
|
||||||
|
getPendingFeedbackOrderIds,
|
||||||
getWorkOrderShare,
|
getWorkOrderShare,
|
||||||
getWorkerCancelRequestRisk,
|
getWorkerCancelRequestRisk,
|
||||||
getWorkerOrderOverview,
|
getWorkerOrderOverview,
|
||||||
@@ -61,26 +64,47 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
|||||||
const myShares = await listWorkerSharesByWorker(worker.id)
|
const myShares = await listWorkerSharesByWorker(worker.id)
|
||||||
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
||||||
const workOrderIds = items.map((item) => Number(item.id))
|
const workOrderIds = items.map((item) => Number(item.id))
|
||||||
const [notes, views] = await Promise.all([
|
const [notes, views, pendingCancelOrderIds, pendingFeedbackOrderIds, materialEventAtByOrderId] =
|
||||||
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
await Promise.all([
|
||||||
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
||||||
])
|
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
||||||
|
getPendingCancelRequestOrderIds(worker.id, workOrderIds),
|
||||||
|
getPendingFeedbackOrderIds(worker.id, workOrderIds),
|
||||||
|
getLatestMaterialEventAtByOrderIds(workOrderIds),
|
||||||
|
])
|
||||||
const noteByOrderId = new Map(
|
const noteByOrderId = new Map(
|
||||||
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
||||||
)
|
)
|
||||||
const viewedAtByOrderId = new Map(
|
const viewedAtByOrderId = new Map(
|
||||||
views.map((item) => [Number(item.work_order_id), String(item.viewed_at || '')]),
|
views.map((item) => [Number(item.work_order_id), String(item.viewed_at || '')]),
|
||||||
)
|
)
|
||||||
|
const materialSeenAtByOrderId = new Map(
|
||||||
|
views.map((item) => [Number(item.work_order_id), String(item.material_seen_at || '')]),
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
items: items.map((item) =>
|
items: items.map((item) => {
|
||||||
mapWorkOrderForWorker(
|
const workOrderId = Number(item.id)
|
||||||
item,
|
const materialEventAt = materialEventAtByOrderId.get(workOrderId) || ''
|
||||||
permissions,
|
const materialSeenAt = materialSeenAtByOrderId.get(workOrderId) || ''
|
||||||
myShareByOrderId.get(Number(item.id)),
|
const materialUpdated =
|
||||||
noteByOrderId.get(Number(item.id)) || '',
|
Boolean(materialEventAt) &&
|
||||||
viewedAtByOrderId.get(Number(item.id)) || null,
|
(!materialSeenAt ||
|
||||||
),
|
new Date(materialEventAt).getTime() > new Date(materialSeenAt).getTime())
|
||||||
),
|
return {
|
||||||
|
...mapWorkOrderForWorker(
|
||||||
|
item,
|
||||||
|
permissions,
|
||||||
|
myShareByOrderId.get(workOrderId),
|
||||||
|
noteByOrderId.get(workOrderId) || '',
|
||||||
|
viewedAtByOrderId.get(workOrderId) || null,
|
||||||
|
),
|
||||||
|
// 问题单分组内的来源角标:平台标记 / 撤单审核中 / 反馈处理中。
|
||||||
|
problemMarked: item.status === 'problem',
|
||||||
|
pendingCancelRequest: pendingCancelOrderIds.has(workOrderId),
|
||||||
|
pendingFeedback: pendingFeedbackOrderIds.has(workOrderId),
|
||||||
|
materialUpdated,
|
||||||
|
}
|
||||||
|
}),
|
||||||
pagination: { page, pageSize, total },
|
pagination: { page, pageSize, total },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,8 +343,12 @@ export async function saveWorkerOrderNote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标记打手本人已查看工单,首次操作后不再展示“新”标识。 */
|
/** 标记打手本人已查看工单,首次操作后不再展示“新”标识;materialSeen 同时刷新资料查看时间。 */
|
||||||
export async function markWorkerOrderRead(workOrderId: number | string, session: WorkerSession) {
|
export async function markWorkerOrderRead(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
const workerId = Number(session.workerId)
|
const workerId = Number(session.workerId)
|
||||||
@@ -339,6 +367,7 @@ export async function markWorkerOrderRead(workOrderId: number | string, session:
|
|||||||
workOrderId: Number(workOrder.id),
|
workOrderId: Number(workOrder.id),
|
||||||
workerId,
|
workerId,
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
|
materialSeen: payload.materialSeen === true || payload.materialSeen === 'true',
|
||||||
})
|
})
|
||||||
publishWorkOrderRealtimeChange({
|
publishWorkOrderRealtimeChange({
|
||||||
workOrderId: Number(workOrder.id),
|
workOrderId: Number(workOrder.id),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
getDisplayOrderNo,
|
getDisplayOrderNo,
|
||||||
getMaterialDetailItems,
|
getMaterialDetailItems,
|
||||||
getMaterialScreenshots,
|
getMaterialScreenshots,
|
||||||
|
getOrderFlagTags,
|
||||||
getOrderStatusDisplay,
|
getOrderStatusDisplay,
|
||||||
getSettlementLabel,
|
getSettlementLabel,
|
||||||
getUniqueAcceptanceFiles,
|
getUniqueAcceptanceFiles,
|
||||||
@@ -228,6 +229,7 @@ function WorkerOrderMobileCard({
|
|||||||
const noteText = getWorkerAcceptance(order)?.note
|
const noteText = getWorkerAcceptance(order)?.note
|
||||||
const workerNote = String(order.workerNote || '').trim()
|
const workerNote = String(order.workerNote || '').trim()
|
||||||
const statusDisplay = getOrderStatusDisplay(order)
|
const statusDisplay = getOrderStatusDisplay(order)
|
||||||
|
const flagTags = getOrderFlagTags(order)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="worker-order-mobile-card">
|
<article className="worker-order-mobile-card">
|
||||||
@@ -248,6 +250,11 @@ function WorkerOrderMobileCard({
|
|||||||
拼单
|
拼单
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
|
{flagTags.map((flag) => (
|
||||||
|
<Tag key={flag.key} color={flag.color} className="worker-order-mobile-tag">
|
||||||
|
{flag.label}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
<span className="worker-order-mobile-orderno-text">{getDisplayOrderNo(order)}</span>
|
<span className="worker-order-mobile-orderno-text">{getDisplayOrderNo(order)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="worker-order-mobile-header-right">
|
<div className="worker-order-mobile-header-right">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
getDisplayOrderNo,
|
getDisplayOrderNo,
|
||||||
getMaterialDetailItems,
|
getMaterialDetailItems,
|
||||||
getMaterialScreenshots,
|
getMaterialScreenshots,
|
||||||
|
getOrderFlagTags,
|
||||||
getOrderStatusDisplay,
|
getOrderStatusDisplay,
|
||||||
getUniqueAcceptanceFiles,
|
getUniqueAcceptanceFiles,
|
||||||
getWorkerAcceptance,
|
getWorkerAcceptance,
|
||||||
@@ -102,6 +103,7 @@ export function buildWorkerOrderTableColumns({
|
|||||||
width: 130,
|
width: 130,
|
||||||
render: (_, row) => {
|
render: (_, row) => {
|
||||||
const statusDisplay = getOrderStatusDisplay(row)
|
const statusDisplay = getOrderStatusDisplay(row)
|
||||||
|
const flagTags = getOrderFlagTags(row)
|
||||||
return (
|
return (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Space wrap size={4}>
|
<Space wrap size={4}>
|
||||||
@@ -115,6 +117,15 @@ export function buildWorkerOrderTableColumns({
|
|||||||
<GiftCountdownTag giftAvailableAt={row.gift.giftAvailableAt} />
|
<GiftCountdownTag giftAvailableAt={row.gift.giftAvailableAt} />
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
|
{flagTags.length > 0 ? (
|
||||||
|
<Space wrap size={4}>
|
||||||
|
{flagTags.map((flag) => (
|
||||||
|
<Tag key={flag.key} color={flag.color}>
|
||||||
|
{flag.label}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : null}
|
||||||
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
|
{row.status === 'problem' && String(row.problemNote || '').trim() ? (
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
type="danger"
|
type="danger"
|
||||||
|
|||||||
@@ -223,13 +223,19 @@ export default function WorkerOrdersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markOrderAsRead(order: WorkOrder) {
|
function markOrderAsRead(order: WorkOrder) {
|
||||||
if (!order.isUnread) return
|
if (order.isUnread) {
|
||||||
queryClient.setQueriesData({ queryKey: ['worker-my-orders'] }, (current: unknown) =>
|
queryClient.setQueriesData({ queryKey: ['worker-my-orders'] }, (current: unknown) =>
|
||||||
patchWorkOrderReadState(current, order.workOrderId),
|
patchWorkOrderReadState(current, order.workOrderId),
|
||||||
)
|
)
|
||||||
void markWorkerOrderRead(order.workOrderId).catch(() => {
|
}
|
||||||
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
// 每次打开详情都刷新资料查看时间,用于清除"资料已更新"角标。
|
||||||
})
|
void markWorkerOrderRead(order.workOrderId, { materialSeen: true })
|
||||||
|
.then(() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['worker-my-orders'] })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openOrderDetail(order: WorkOrder) {
|
function openOrderDetail(order: WorkOrder) {
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ export function getOrderStatusDisplay(order: WorkOrder): { label: string; color:
|
|||||||
return { label: formatStatus(order.status), color: resolveStatusColor(order.status) }
|
return { label: formatStatus(order.status), color: resolveStatusColor(order.status) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 问题单来源与资料更新的订单角标,桌面表格与移动卡片共用。 */
|
||||||
|
export function getOrderFlagTags(order: WorkOrder) {
|
||||||
|
const flags: Array<{ key: string; label: string; color: string }> = []
|
||||||
|
if (order.problemMarked) flags.push({ key: 'problem', label: '平台标记', color: 'red' })
|
||||||
|
if (order.pendingCancelRequest)
|
||||||
|
flags.push({ key: 'cancel', label: '撤单审核中', color: 'orange' })
|
||||||
|
if (order.pendingFeedback) flags.push({ key: 'feedback', label: '反馈处理中', color: 'orange' })
|
||||||
|
if (order.materialUpdated) flags.push({ key: 'material', label: '资料已更新', color: 'blue' })
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
export function getOrderStatusHint(order: WorkOrder) {
|
export function getOrderStatusHint(order: WorkOrder) {
|
||||||
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
|
if (isFriendGiftOrder(order) && order.status === 'in_progress' && order.gift) {
|
||||||
const hints: Record<string, string> = {
|
const hints: Record<string, string> = {
|
||||||
|
|||||||
@@ -270,8 +270,11 @@ export function submitWorkerOrderFeedback(workOrderId: number, payload: { conten
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markWorkerOrderRead(workOrderId: number) {
|
export function markWorkerOrderRead(workOrderId: number, options: { materialSeen?: boolean } = {}) {
|
||||||
return apiPost<{ viewedAt: string; isUnread: false }>(`/api/v1/worker/orders/${workOrderId}/read`)
|
return apiPost<{ viewedAt: string; isUnread: false }>(
|
||||||
|
`/api/v1/worker/orders/${workOrderId}/read`,
|
||||||
|
{ materialSeen: options.materialSeen === true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveWorkerOrderNote(workOrderId: number, note: string) {
|
export function saveWorkerOrderNote(workOrderId: number, note: string) {
|
||||||
|
|||||||
@@ -548,6 +548,11 @@ export type WorkOrder = {
|
|||||||
/** 打手本人尚未点击查看或操作过该工单。 */
|
/** 打手本人尚未点击查看或操作过该工单。 */
|
||||||
isUnread?: boolean
|
isUnread?: boolean
|
||||||
problemNote: string
|
problemNote: string
|
||||||
|
/** 问题单分组来源角标:平台标记 / 撤单审核中 / 反馈处理中 / 资料已更新。 */
|
||||||
|
problemMarked?: boolean
|
||||||
|
pendingCancelRequest?: boolean
|
||||||
|
pendingFeedback?: boolean
|
||||||
|
materialUpdated?: boolean
|
||||||
/** 取消信息,仅已取消工单返回。 */
|
/** 取消信息,仅已取消工单返回。 */
|
||||||
cancel?: {
|
cancel?: {
|
||||||
reason: string
|
reason: string
|
||||||
|
|||||||
Reference in New Issue
Block a user