增加接单工单客服备注
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- 061_work_order_admin_note.sql —— 后台客服工单备注。
|
||||||
|
ALTER TABLE work_orders
|
||||||
|
ADD COLUMN IF NOT EXISTS admin_note TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN work_orders.admin_note IS '后台客服维护的工单备注,不展示给打手';
|
||||||
@@ -355,6 +355,7 @@ export type WorkOrderRow = {
|
|||||||
requirement_json: string | Record<string, unknown>
|
requirement_json: string | Record<string, unknown>
|
||||||
acceptance_json: string | Record<string, unknown>
|
acceptance_json: string | Record<string, unknown>
|
||||||
draft_acceptance_json: string | Record<string, unknown>
|
draft_acceptance_json: string | Record<string, unknown>
|
||||||
|
admin_note: string
|
||||||
problem_note: string
|
problem_note: string
|
||||||
sharing_enabled: boolean
|
sharing_enabled: boolean
|
||||||
sharing_total_quantity: number
|
sharing_total_quantity: number
|
||||||
|
|||||||
@@ -167,6 +167,19 @@ export async function updateWorkOrderBasic(
|
|||||||
return getWorkOrderById(workOrderId)
|
return getWorkOrderById(workOrderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateWorkOrderAdminNote(
|
||||||
|
workOrderId: number | string,
|
||||||
|
note: string,
|
||||||
|
updatedAt: string,
|
||||||
|
): Promise<WorkOrderRow | null> {
|
||||||
|
const result = await query<{ id: number }>(
|
||||||
|
`UPDATE work_orders SET admin_note = $1, updated_at = $2 WHERE id = $3 RETURNING id`,
|
||||||
|
[String(note || '').trim(), updatedAt, Number(workOrderId)],
|
||||||
|
)
|
||||||
|
if (!result.rows[0]) return null
|
||||||
|
return getWorkOrderById(result.rows[0].id)
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteWorkOrder(workOrderId: number | string): Promise<boolean> {
|
export async function deleteWorkOrder(workOrderId: number | string): Promise<boolean> {
|
||||||
const result = await query('DELETE FROM work_orders WHERE id = $1', [Number(workOrderId)])
|
const result = await query('DELETE FROM work_orders WHERE id = $1', [Number(workOrderId)])
|
||||||
return Number(result.rowCount || 0) > 0
|
return Number(result.rowCount || 0) > 0
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
unassignAdminWorkOrder,
|
unassignAdminWorkOrder,
|
||||||
unpublishAdminWorkOrder,
|
unpublishAdminWorkOrder,
|
||||||
updateAdminWorkOrder,
|
updateAdminWorkOrder,
|
||||||
|
updateAdminWorkOrderNote,
|
||||||
updateAdminWorkOrderSharing,
|
updateAdminWorkOrderSharing,
|
||||||
} from '../../../services/worker-platform/index.js'
|
} from '../../../services/worker-platform/index.js'
|
||||||
import {
|
import {
|
||||||
@@ -188,6 +189,30 @@ router.put(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/worker-platform/orders/:workOrderId/note',
|
||||||
|
requireAdminPermission('worker_order.edit'),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
updateAdminWorkOrderNote(
|
||||||
|
String(req.params.workOrderId || ''),
|
||||||
|
req.body?.note,
|
||||||
|
req.adminSession?.username || '',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '工单备注已保存',
|
||||||
|
errorMessage: '保存工单备注失败',
|
||||||
|
scope: '[admin/worker-platform/orders/:workOrderId/note]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'work_order_admin_note_updated',
|
||||||
|
targetType: 'work_order',
|
||||||
|
targetId: String(req.params.workOrderId || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/assign',
|
'/worker-platform/orders/:workOrderId/assign',
|
||||||
requireAdminPermission('worker_order.assign'),
|
requireAdminPermission('worker_order.assign'),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
resolveProblemWorkOrder,
|
resolveProblemWorkOrder,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkOrderBasic,
|
updateWorkOrderBasic,
|
||||||
|
updateWorkOrderAdminNote,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
@@ -111,6 +112,40 @@ export async function updateAdminWorkOrder(
|
|||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAdminWorkOrderNote(
|
||||||
|
workOrderId: number | string,
|
||||||
|
note: unknown,
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const normalizedNote = String(note ?? '').trim()
|
||||||
|
if (normalizedNote.length > 2000) {
|
||||||
|
throw createHttpError('备注不能超过 2000 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_admin_note_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkOrderAdminNote(workOrder.id, normalizedNote, now)
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('保存工单备注失败', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_admin_note_update_failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: 'admin_note_updated',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({ note: normalizedNote }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
return { order: mapWorkOrderAdmin(updated) }
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canDelete) {
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canDelete) {
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
|
|||||||
},
|
},
|
||||||
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
|
||||||
draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json),
|
draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json),
|
||||||
|
adminNote: workOrder.admin_note || '',
|
||||||
problemNote: workOrder.problem_note || '',
|
problemNote: workOrder.problem_note || '',
|
||||||
cancel:
|
cancel:
|
||||||
workOrder.status === WORK_ORDER_STATUS.CANCELLED
|
workOrder.status === WORK_ORDER_STATUS.CANCELLED
|
||||||
@@ -286,6 +287,7 @@ export function mapWorkOrderForWorker(
|
|||||||
rawStatus: _rawStatus,
|
rawStatus: _rawStatus,
|
||||||
hallAvailable: _hallAvailable,
|
hallAvailable: _hallAvailable,
|
||||||
capabilities: _capabilities,
|
capabilities: _capabilities,
|
||||||
|
adminNote: _adminNote,
|
||||||
...visibleOrder
|
...visibleOrder
|
||||||
} = mapped
|
} = mapped
|
||||||
const personalStatus = resolveWorkerShareStatus(workOrder.status, myShare?.status)
|
const personalStatus = resolveWorkerShareStatus(workOrder.status, myShare?.status)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
submitAdminWorkOrderMaterial,
|
submitAdminWorkOrderMaterial,
|
||||||
unpublishAdminWorkOrder,
|
unpublishAdminWorkOrder,
|
||||||
updateAdminWorkOrder,
|
updateAdminWorkOrder,
|
||||||
|
updateAdminWorkOrderNote,
|
||||||
updateAdminWorkOrderSharing,
|
updateAdminWorkOrderSharing,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
||||||
@@ -223,6 +224,7 @@ export default function WorkOrdersPanel() {
|
|||||||
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)
|
||||||
|
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||||
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
|
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
|
||||||
const [problemForm] = Form.useForm()
|
const [problemForm] = Form.useForm()
|
||||||
@@ -241,6 +243,7 @@ export default function WorkOrdersPanel() {
|
|||||||
timeoutMinutes?: number
|
timeoutMinutes?: number
|
||||||
timeoutPolicy?: string
|
timeoutPolicy?: string
|
||||||
}>()
|
}>()
|
||||||
|
const [noteForm] = Form.useForm<{ note?: string }>()
|
||||||
const [sharingForm] = Form.useForm<{
|
const [sharingForm] = Form.useForm<{
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
totalQuantity?: number
|
totalQuantity?: number
|
||||||
@@ -710,6 +713,25 @@ export default function WorkOrdersPanel() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNoteModal(row: WorkOrder) {
|
||||||
|
setNoteOrder(row)
|
||||||
|
noteForm.setFieldsValue({ note: row.adminNote || '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitNote(values: { note?: string }) {
|
||||||
|
if (!noteOrder) return
|
||||||
|
const succeeded = await runAction(
|
||||||
|
() => updateAdminWorkOrderNote(noteOrder.workOrderId, String(values.note || '')),
|
||||||
|
'工单备注已保存',
|
||||||
|
)
|
||||||
|
if (!succeeded) return
|
||||||
|
if (detailOrder?.workOrderId === noteOrder.workOrderId) {
|
||||||
|
setDetailOrder({ ...detailOrder, adminNote: String(values.note || '').trim() })
|
||||||
|
}
|
||||||
|
setNoteOrder(null)
|
||||||
|
noteForm.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
function openDeductModal(row: WorkOrder) {
|
function openDeductModal(row: WorkOrder) {
|
||||||
setDeductOrder(row)
|
setDeductOrder(row)
|
||||||
deductForm.setFieldsValue({
|
deductForm.setFieldsValue({
|
||||||
@@ -974,6 +996,15 @@ export default function WorkOrdersPanel() {
|
|||||||
{cancelText}
|
{cancelText}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
{String(row.adminNote || '').trim() ? (
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, maxWidth: 140 }}
|
||||||
|
ellipsis={{ tooltip: row.adminNote }}
|
||||||
|
>
|
||||||
|
备注:{row.adminNote}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1027,6 +1058,11 @@ export default function WorkOrdersPanel() {
|
|||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
||||||
|
{canEditOrder ? (
|
||||||
|
<Button icon={<FormOutlined />} onClick={() => openNoteModal(row)}>
|
||||||
|
备注
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
{row.capabilities?.canEdit && canEditOrder ? (
|
{row.capabilities?.canEdit && canEditOrder ? (
|
||||||
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
|
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
|
||||||
编辑
|
编辑
|
||||||
@@ -1502,6 +1538,22 @@ export default function WorkOrdersPanel() {
|
|||||||
{String(detailOrder.problemNote || '').trim() || '-'}
|
{String(detailOrder.problemNote || '').trim() || '-'}
|
||||||
</span>
|
</span>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="客服备注" span={2}>
|
||||||
|
<Space align="start">
|
||||||
|
<span className="worker-order-detail-value">
|
||||||
|
{String(detailOrder.adminNote || '').trim() || '-'}
|
||||||
|
</span>
|
||||||
|
{canEditOrder ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => openNoteModal(detailOrder)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
{detailOrder.cancel ? (
|
{detailOrder.cancel ? (
|
||||||
<Descriptions.Item label="取消信息" span={2}>
|
<Descriptions.Item label="取消信息" span={2}>
|
||||||
<span className="worker-order-detail-value">
|
<span className="worker-order-detail-value">
|
||||||
@@ -1683,6 +1735,34 @@ export default function WorkOrdersPanel() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
<Modal
|
||||||
|
title={`客服备注 · ${getDisplayOrderNo(noteOrder)}`}
|
||||||
|
open={Boolean(noteOrder)}
|
||||||
|
onCancel={() => {
|
||||||
|
setNoteOrder(null)
|
||||||
|
noteForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => noteForm.submit()}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
{noteOrder ? (
|
||||||
|
<Form form={noteForm} layout="vertical" onFinish={submitNote}>
|
||||||
|
<Form.Item
|
||||||
|
label="备注内容"
|
||||||
|
name="note"
|
||||||
|
rules={[{ max: 2000, message: '备注不能超过 2000 个字符' }]}
|
||||||
|
extra="仅后台客服可见,不会展示给打手。"
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={5}
|
||||||
|
maxLength={2000}
|
||||||
|
showCount
|
||||||
|
placeholder="请输入本单客服备注"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
title={`编辑工单 · ${getDisplayOrderNo(editOrder)}`}
|
title={`编辑工单 · ${getDisplayOrderNo(editOrder)}`}
|
||||||
open={Boolean(editOrder)}
|
open={Boolean(editOrder)}
|
||||||
|
|||||||
@@ -628,6 +628,12 @@ export function updateAdminWorkOrder(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateAdminWorkOrderNote(workOrderId: number, note: string) {
|
||||||
|
return apiPut<{ order: WorkOrder }>(`/api/v1/admin/worker-platform/orders/${workOrderId}/note`, {
|
||||||
|
note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteAdminWorkOrder(workOrderId: number) {
|
export function deleteAdminWorkOrder(workOrderId: number) {
|
||||||
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/orders/${workOrderId}`)
|
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/orders/${workOrderId}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,6 +576,8 @@ export type WorkOrder = {
|
|||||||
acceptance: WorkerAcceptance
|
acceptance: WorkerAcceptance
|
||||||
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
/** 暂存的验收草稿(未提交时保存于此,刷新不丢失;提交后清空) */
|
||||||
draftAcceptance: WorkerAcceptance
|
draftAcceptance: WorkerAcceptance
|
||||||
|
/** 后台客服维护的工单备注。 */
|
||||||
|
adminNote?: string
|
||||||
workerNote?: string
|
workerNote?: string
|
||||||
/** 打手本人尚未点击查看或操作过该工单。 */
|
/** 打手本人尚未点击查看或操作过该工单。 */
|
||||||
isUnread?: boolean
|
isUnread?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user