适配来源消息只读任务展示
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
</span>
|
||||
<div class="task-links">
|
||||
<RouterLink
|
||||
v-if="!isSourceMessageOnlyDetail"
|
||||
class="task-link"
|
||||
:to="`/reservation/orders/${detail.order_id}`"
|
||||
>
|
||||
@@ -74,6 +75,50 @@
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="isSourceMessageOnlyDetail"
|
||||
class="source-only-result"
|
||||
>
|
||||
<header>
|
||||
<h2>{{ t('task.sourceOnlyResult') }}</h2>
|
||||
<p>{{ t('task.sourceOnlyReadonly') }}</p>
|
||||
</header>
|
||||
<dl v-if="sourceMessageOnlyResult">
|
||||
<div>
|
||||
<dt>{{ t('task.entryResultCode') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ sourceMessageOnlyResult.entry_result_code || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.entryResultMeaning') }}</dt>
|
||||
<dd>{{ sourceMessageOnlyResult.entry_result_meaning || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.entryResultDescription') }}</dt>
|
||||
<dd>{{ sourceMessageOnlyResult.entry_result_description || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.entryResultSourceMessageId') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ sourceMessageOnlyResult.entry_result_source_message_id || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="source-only-result__raw">
|
||||
<dt>{{ t('task.rawAnswer') }}</dt>
|
||||
<dd>
|
||||
<pre>{{ sourceMessageOnlyResult.raw_answer || '-' }}</pre>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p
|
||||
v-else
|
||||
class="source-only-result__empty"
|
||||
>
|
||||
{{ t('task.noSourceOnlyResult') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="detail.availability.blocked"
|
||||
class="task-alert"
|
||||
@@ -105,7 +150,7 @@
|
||||
<div class="task-detail-body">
|
||||
<div class="task-card-main">
|
||||
<ReservationManualReviewConversionPanel
|
||||
v-if="isManualReviewTask"
|
||||
v-if="isManualReviewTask && !isSourceMessageOnlyDetail"
|
||||
:busy="mutationBusy !== null"
|
||||
@submit="submitManualReviewConversion"
|
||||
/>
|
||||
@@ -152,6 +197,7 @@
|
||||
|
||||
<aside class="task-side-stack">
|
||||
<ReservationOperaPanel
|
||||
v-if="!isSourceMessageOnlyDetail"
|
||||
:task-id="detail.task_id"
|
||||
:operations="detail.opera_operations"
|
||||
:executable="detail.availability.executable && canExecuteOpera"
|
||||
@@ -198,7 +244,11 @@ import {
|
||||
validateReservationFieldValues,
|
||||
type ReservationFieldValidationErrors,
|
||||
} from '@/utils/reservationFieldRules'
|
||||
import { formatReservationMaybeStableReason, formatReservationTaskCard } from '@/utils/reservationDisplay'
|
||||
import {
|
||||
formatReservationMaybeStableReason,
|
||||
formatReservationTaskCard,
|
||||
isReservationSourceMessageOnlyTask,
|
||||
} from '@/utils/reservationDisplay'
|
||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
@@ -219,9 +269,17 @@ const validationErrors = ref<ReservationFieldValidationErrors>({})
|
||||
const validationSummaryActive = ref(false)
|
||||
const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | null>(null)
|
||||
|
||||
const canEditTask = computed(() => authStore.hasPermission('RESERVATION_TASK_EDIT'))
|
||||
const canConfirmTask = computed(() => authStore.hasPermission('RESERVATION_TASK_CONFIRM'))
|
||||
const canExecuteOpera = computed(() => authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE'))
|
||||
const isSourceMessageOnlyDetail = computed(() =>
|
||||
isReservationSourceMessageOnlyTask(detail.value?.system_task_type, detail.value?.task_card_type),
|
||||
)
|
||||
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
|
||||
const canEditTask = computed(() => authStore.hasPermission('RESERVATION_TASK_EDIT') && !isSourceMessageOnlyDetail.value)
|
||||
const canConfirmTask = computed(
|
||||
() => authStore.hasPermission('RESERVATION_TASK_CONFIRM') && !isSourceMessageOnlyDetail.value,
|
||||
)
|
||||
const canExecuteOpera = computed(
|
||||
() => authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE') && !isSourceMessageOnlyDetail.value,
|
||||
)
|
||||
const canViewAudit = computed(() => authStore.hasPermission('RESERVATION_AUDIT_READ'))
|
||||
const canSave = computed(() => canEditTask.value && Boolean(detail.value?.availability.editable))
|
||||
const canConfirm = computed(() => canConfirmTask.value && Boolean(detail.value?.availability.confirmable))
|
||||
@@ -503,35 +561,81 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.source-only-result {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--th-color-blue-100);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-blue-100);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-evidence h2,
|
||||
.task-evidence dl {
|
||||
.task-evidence dl,
|
||||
.source-only-result h2,
|
||||
.source-only-result p,
|
||||
.source-only-result dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-evidence h2 {
|
||||
.task-evidence h2,
|
||||
.source-only-result h2 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.source-only-result header {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.source-only-result header p,
|
||||
.source-only-result__empty {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.task-evidence dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.source-only-result dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-evidence dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-evidence dd {
|
||||
.task-evidence dd,
|
||||
.source-only-result dd {
|
||||
margin: 5px 0 0;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.source-only-result__raw {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.source-only-result pre {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.task-alert,
|
||||
.task-notice {
|
||||
display: flex;
|
||||
@@ -617,7 +721,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.task-evidence dl {
|
||||
.task-evidence dl,
|
||||
.source-only-result dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,9 @@ export default {
|
||||
viewTask: 'View task',
|
||||
viewOrder: 'View order',
|
||||
viewConversation: 'View email conversation',
|
||||
legacyMessageNotification: 'Message notification (legacy)',
|
||||
sourceOnlyOrderLabel: 'Source message only',
|
||||
hiddenTechnicalOrder: 'No visible order',
|
||||
columns: {
|
||||
task: 'Task',
|
||||
order: 'Order',
|
||||
@@ -312,6 +315,15 @@ export default {
|
||||
sourceSender: 'Sender',
|
||||
sourceReceivedAt: 'Received at',
|
||||
sourceSummaryPending: 'Source email summary fields are pending backend support',
|
||||
sourceOnlyResult: 'Source message entry result',
|
||||
sourceOnlyReadonly: 'This task only shows the source message entry result. It does not enter order handling, field editing, or OPERA operations.',
|
||||
sourceOnlyNoOpera: 'This task type does not involve OPERA operations.',
|
||||
entryResultCode: 'Entry result code',
|
||||
entryResultMeaning: 'Result meaning',
|
||||
entryResultDescription: 'Result description',
|
||||
entryResultSourceMessageId: 'Result source message ID',
|
||||
rawAnswer: 'SuperAgent raw answer',
|
||||
noSourceOnlyResult: 'No source message entry result is available.',
|
||||
manualConversion: {
|
||||
title: 'Manual conversion',
|
||||
description: 'Convert a Fallback / manual review task into a processable reservation task.',
|
||||
@@ -424,6 +436,8 @@ export default {
|
||||
TRACE_NOTE: 'Trace / notes',
|
||||
TA_RECORD: 'TA record',
|
||||
MESSAGE_NOTIFICATION: 'Message notification',
|
||||
INFORMATIONAL_MESSAGE: 'Message notification (legacy)',
|
||||
SOURCE_MESSAGE_ONLY: 'Source message only',
|
||||
MANUAL_REVIEW: 'Manual review',
|
||||
FALLBACK_REVIEW: 'Manual review',
|
||||
},
|
||||
@@ -438,6 +452,8 @@ export default {
|
||||
NEW_GROUP_BLOCK: 'New group block',
|
||||
NEW_FIT_RESERVATION: 'New FIT reservation',
|
||||
MANUAL_REVIEW: 'Manual review',
|
||||
S000: 'Pure informational email',
|
||||
S999: 'Business material package unavailable',
|
||||
},
|
||||
readonlyReason: {
|
||||
PROCESSABLE: 'Processable',
|
||||
|
||||
@@ -268,6 +268,9 @@ export default {
|
||||
viewTask: 'ดูงาน',
|
||||
viewOrder: 'ดูออเดอร์',
|
||||
viewConversation: 'ดูเธรดอีเมล',
|
||||
legacyMessageNotification: 'การแจ้งเตือนข้อความ (เดิม)',
|
||||
sourceOnlyOrderLabel: 'ข้อความต้นทางแบบอ่านอย่างเดียว',
|
||||
hiddenTechnicalOrder: 'ไม่มีออเดอร์ที่แสดงได้',
|
||||
columns: {
|
||||
task: 'งาน',
|
||||
order: 'ออเดอร์',
|
||||
@@ -312,6 +315,15 @@ export default {
|
||||
sourceSender: 'ผู้ส่ง',
|
||||
sourceReceivedAt: 'เวลารับ',
|
||||
sourceSummaryPending: 'ข้อมูลสรุปอีเมลต้นทางยังรอหลังบ้านเพิ่มเติม',
|
||||
sourceOnlyResult: 'ผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
||||
sourceOnlyReadonly: 'งานนี้ใช้แสดงผลลัพธ์ทางเข้าของข้อความต้นทางเท่านั้น ไม่เข้าสู่การจัดการออเดอร์ การแก้ไขข้อมูล หรือ OPERA',
|
||||
sourceOnlyNoOpera: 'งานประเภทนี้ไม่เกี่ยวข้องกับ OPERA',
|
||||
entryResultCode: 'รหัสผลลัพธ์ทางเข้า',
|
||||
entryResultMeaning: 'ความหมายผลลัพธ์',
|
||||
entryResultDescription: 'คำอธิบายผลลัพธ์',
|
||||
entryResultSourceMessageId: 'รหัสข้อความต้นทางของผลลัพธ์',
|
||||
rawAnswer: 'คำตอบดิบจาก SuperAgent',
|
||||
noSourceOnlyResult: 'ยังไม่มีผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
||||
manualConversion: {
|
||||
title: 'แปลงงานด้วยคน',
|
||||
description: 'แปลงงาน Fallback / งานตรวจสอบด้วยคนให้เป็นงานการจองที่ประมวลผลได้',
|
||||
@@ -424,6 +436,8 @@ export default {
|
||||
TRACE_NOTE: 'หมายเหตุติดตาม (Trace)',
|
||||
TA_RECORD: 'ข้อมูลบริษัททัวร์',
|
||||
MESSAGE_NOTIFICATION: 'แจ้งเตือนข้อความ',
|
||||
INFORMATIONAL_MESSAGE: 'แจ้งเตือนข้อความ (เดิม)',
|
||||
SOURCE_MESSAGE_ONLY: 'ข้อความต้นทางแบบอ่านอย่างเดียว',
|
||||
MANUAL_REVIEW: 'ตรวจสอบด้วยคน',
|
||||
FALLBACK_REVIEW: 'ตรวจสอบด้วยคน',
|
||||
},
|
||||
@@ -438,6 +452,8 @@ export default {
|
||||
NEW_GROUP_BLOCK: 'บล็อกกรุ๊ปใหม่',
|
||||
NEW_FIT_RESERVATION: 'จองรายบุคคลใหม่',
|
||||
MANUAL_REVIEW: 'ตรวจสอบด้วยคน',
|
||||
S000: 'อีเมลข้อมูลทั่วไป',
|
||||
S999: 'ไม่สามารถสร้างชุดข้อมูลธุรกิจได้',
|
||||
},
|
||||
readonlyReason: {
|
||||
PROCESSABLE: 'ประมวลผลได้',
|
||||
|
||||
@@ -268,6 +268,9 @@ export default {
|
||||
viewTask: '查看任务',
|
||||
viewOrder: '查看订单',
|
||||
viewConversation: '查看邮件会话',
|
||||
legacyMessageNotification: '消息通知(历史 Message Notification)',
|
||||
sourceOnlyOrderLabel: '来源消息只读',
|
||||
hiddenTechnicalOrder: '不关联可见订单',
|
||||
columns: {
|
||||
task: '任务',
|
||||
order: '订单',
|
||||
@@ -312,6 +315,15 @@ export default {
|
||||
sourceSender: '发件人',
|
||||
sourceReceivedAt: '接收时间',
|
||||
sourceSummaryPending: '来源邮件摘要字段待后端补充',
|
||||
sourceOnlyResult: '来源消息入口结果',
|
||||
sourceOnlyReadonly: '该任务仅用于展示来源消息入口结果,不进入订单处理、字段编辑或 OPERA 操作。',
|
||||
sourceOnlyNoOpera: '该类任务不涉及 OPERA 操作。',
|
||||
entryResultCode: '入口结果码',
|
||||
entryResultMeaning: '结果含义',
|
||||
entryResultDescription: '结果说明',
|
||||
entryResultSourceMessageId: '结果来源消息编号',
|
||||
rawAnswer: 'SuperAgent 原始回答',
|
||||
noSourceOnlyResult: '暂无来源消息入口结果。',
|
||||
manualConversion: {
|
||||
title: '人工转换',
|
||||
description: '将人工复核任务转换为可处理的预订任务。',
|
||||
@@ -424,6 +436,8 @@ export default {
|
||||
TRACE_NOTE: '追踪备注(Trace)',
|
||||
TA_RECORD: '旅行社记录',
|
||||
MESSAGE_NOTIFICATION: '消息通知',
|
||||
INFORMATIONAL_MESSAGE: '消息通知(历史 Message Notification)',
|
||||
SOURCE_MESSAGE_ONLY: '来源消息只读',
|
||||
MANUAL_REVIEW: '人工复核',
|
||||
FALLBACK_REVIEW: '人工复核',
|
||||
},
|
||||
@@ -438,6 +452,8 @@ export default {
|
||||
NEW_GROUP_BLOCK: '新团队预留',
|
||||
NEW_FIT_RESERVATION: '新散客预订',
|
||||
MANUAL_REVIEW: '人工复核',
|
||||
S000: '纯信息类邮件',
|
||||
S999: '无法形成业务素材包',
|
||||
},
|
||||
readonlyReason: {
|
||||
PROCESSABLE: '可处理',
|
||||
|
||||
@@ -66,6 +66,35 @@ function createTaskDetail(overrides: Partial<ReservationTaskDetailResult> = {}):
|
||||
}
|
||||
}
|
||||
|
||||
function createSourceMessageOnlyTaskDetail(
|
||||
overrides: Partial<ReservationTaskDetailResult> = {},
|
||||
): ReservationTaskDetailResult {
|
||||
return createTaskDetail({
|
||||
system_task_type: 'SOURCE_MESSAGE_ONLY',
|
||||
task_card_type: 'SOURCE_MESSAGE_ONLY',
|
||||
task_status: 'COMPLETED',
|
||||
fields: [],
|
||||
opera_operations: [],
|
||||
availability: {
|
||||
blocked: false,
|
||||
read_only: true,
|
||||
editable: false,
|
||||
confirmable: false,
|
||||
executable: false,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: null,
|
||||
},
|
||||
source_message_only_result: {
|
||||
entry_result_code: 'S000',
|
||||
entry_result_meaning: 'PURE_INFORMATION',
|
||||
entry_result_description: '纯信息类邮件',
|
||||
entry_result_source_message_id: '30001',
|
||||
raw_answer: 'S000,30001',
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
function createMutationResult(taskStatus = 'PENDING_CONFIRM'): ReservationTaskPayloadMutationResult {
|
||||
return {
|
||||
task_id: '10001',
|
||||
@@ -428,6 +457,29 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
expect(wrapper.text()).not.toContain('接口待补')
|
||||
})
|
||||
|
||||
it('renders source-message-only entry result without editable or OPERA actions', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createSourceMessageOnlyTaskDetail())
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
expect(wrapper.text()).toContain('来源消息只读')
|
||||
expect(wrapper.text()).toContain('来源消息入口结果')
|
||||
expect(wrapper.text()).toContain('该任务仅用于展示来源消息入口结果')
|
||||
expect(wrapper.text()).toContain('S000')
|
||||
expect(wrapper.text()).toContain('PURE_INFORMATION')
|
||||
expect(wrapper.text()).toContain('纯信息类邮件')
|
||||
expect(wrapper.text()).toContain('S000,30001')
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('查看订单')
|
||||
expect(wrapper.text()).not.toContain('保存草稿')
|
||||
expect(wrapper.text()).not.toContain('确认任务')
|
||||
expect(wrapper.text()).not.toContain('人工转换')
|
||||
expect(wrapper.text()).not.toContain('OPERA 模拟操作')
|
||||
expect(wrapper.text()).not.toContain('确认任务后生成 OPERA 模拟操作')
|
||||
expect(wrapper.find('button[title="执行"]').exists()).toBe(false)
|
||||
expect(wrapper.find('button[title="重试"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('converts a manual review task to a new booking without a target order', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail)
|
||||
.mockResolvedValueOnce(
|
||||
|
||||
@@ -93,6 +93,41 @@ function createTaskListResult(
|
||||
}
|
||||
}
|
||||
|
||||
function createSourceMessageOnlyTaskListResult(taskSubtype: 'S000' | 'S999' = 'S000') {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
task_id: taskSubtype === 'S000' ? '19990' : '19999',
|
||||
order_id: taskSubtype === 'S000' ? '29990' : '29999',
|
||||
hotel_id: 'HOTEL-TEST',
|
||||
display_order_key: null,
|
||||
temporary_order_no: null,
|
||||
task_type: 'SOURCE_MESSAGE_ONLY',
|
||||
task_subtype: taskSubtype,
|
||||
task_status: 'COMPLETED',
|
||||
card_name: 'SOURCE_MESSAGE_ONLY',
|
||||
queue_sequence: null,
|
||||
queue_participation: false,
|
||||
can_process: false,
|
||||
readonly_reason_code: 'QUEUE_EXCLUDED',
|
||||
source_message_id: taskSubtype === 'S000' ? '39990' : '39999',
|
||||
source_subject: taskSubtype === 'S000' ? 'FYI only' : 'Unclear email',
|
||||
source_sender_summary: 'guest@example.test',
|
||||
source_received_at: '2026-07-08T03:00:00Z',
|
||||
external_conversation_id: 'thread-source-only',
|
||||
conversation_message_count: 1,
|
||||
created_at: '2026-07-08T03:00:00Z',
|
||||
updated_at: '2026-07-08T03:10:00Z',
|
||||
},
|
||||
],
|
||||
page: {
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createOrderDetailResult(displayName = 'GRP-001') {
|
||||
return {
|
||||
order: {
|
||||
@@ -440,6 +475,46 @@ describe('reservation P0 views', () => {
|
||||
expect(wrapper.text()).not.toContain('NEW_BOOKING / NEW_BOOKING')
|
||||
})
|
||||
|
||||
it('filters source-message-only tasks with S000 and S999 subtypes', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S000'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.find('select[name="task_type"]').text()).toContain('来源消息只读')
|
||||
expect(wrapper.find('select[name="task_type"]').text()).toContain('历史 Message Notification')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('纯信息类邮件')
|
||||
expect(wrapper.find('select[name="task_subtype"]').text()).toContain('无法形成业务素材包')
|
||||
|
||||
await wrapper.find('select[name="task_type"]').setValue('SOURCE_MESSAGE_ONLY')
|
||||
await vi.dynamicImportSettled()
|
||||
await wrapper.find('select[name="task_subtype"]').setValue('S999')
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(service.fetchReservationTaskList).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
task_type: 'SOURCE_MESSAGE_ONLY',
|
||||
task_subtype: 'S999',
|
||||
page_num: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('renders source-message-only task rows without a visible order action', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList).mockResolvedValue(createSourceMessageOnlyTaskListResult('S000'))
|
||||
|
||||
const wrapper = await mountWithPlugins(ReservationTaskListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(wrapper.text()).toContain('来源消息只读')
|
||||
expect(wrapper.text()).toContain('纯信息类邮件')
|
||||
expect(wrapper.text()).toContain('FYI only')
|
||||
expect(wrapper.text()).toContain('查看任务')
|
||||
expect(wrapper.text()).toContain('查看邮件会话')
|
||||
expect(wrapper.text()).not.toContain('查看订单')
|
||||
expect(wrapper.text()).not.toContain('29990')
|
||||
})
|
||||
|
||||
it('filters and paginates task list with supported backend fields', async () => {
|
||||
vi.mocked(service.fetchReservationTaskList)
|
||||
.mockResolvedValueOnce(createTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 }))
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ReservationTaskCardType =
|
||||
| 'TRACE_NOTE'
|
||||
| 'TA_RECORD'
|
||||
| 'MESSAGE_NOTIFICATION'
|
||||
| 'SOURCE_MESSAGE_ONLY'
|
||||
| 'MANUAL_REVIEW'
|
||||
| 'FALLBACK_REVIEW'
|
||||
|
||||
@@ -311,6 +312,15 @@ export interface ReservationTaskDetailResult {
|
||||
availability: ReservationTaskAvailabilityResult
|
||||
fields: ReservationTaskFieldResult[]
|
||||
opera_operations: ReservationOperaOperationResult[]
|
||||
source_message_only_result?: ReservationSourceMessageOnlyResult | null
|
||||
}
|
||||
|
||||
export interface ReservationSourceMessageOnlyResult {
|
||||
entry_result_code: string | null
|
||||
entry_result_meaning: string | null
|
||||
entry_result_description: string | null
|
||||
entry_result_source_message_id: string | null
|
||||
raw_answer: string | null
|
||||
}
|
||||
|
||||
export interface ReservationTaskPayloadMutationRequest {
|
||||
|
||||
@@ -22,6 +22,10 @@ export function formatReservationTaskTypeSummary(
|
||||
return `${typeLabel} / ${translateStableCode(t, 'taskSubtype', taskSubtype, 'common.unknownTaskSubtype')}`
|
||||
}
|
||||
|
||||
export function isReservationSourceMessageOnlyTask(...codes: Array<string | null | undefined>): boolean {
|
||||
return codes.some((code) => normalizeStableCode(code) === 'SOURCE_MESSAGE_ONLY')
|
||||
}
|
||||
|
||||
export function formatReservationReadonlyReason(
|
||||
t: ReservationTranslator,
|
||||
reasonCode: string | null | undefined,
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
<option value="UPDATE_BOOKING">{{ t('cardType.UPDATE_BOOKING') }}</option>
|
||||
<option value="CANCEL_BOOKING">{{ t('cardType.CANCEL_BOOKING') }}</option>
|
||||
<option value="MANUAL_REVIEW">{{ t('cardType.MANUAL_REVIEW') }}</option>
|
||||
<option value="INFORMATIONAL_MESSAGE">{{ t('cardType.MESSAGE_NOTIFICATION') }}</option>
|
||||
<option value="SOURCE_MESSAGE_ONLY">{{ t('cardType.SOURCE_MESSAGE_ONLY') }}</option>
|
||||
<option value="INFORMATIONAL_MESSAGE">{{ t('taskList.legacyMessageNotification') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -64,6 +65,8 @@
|
||||
<option value="NEW_GROUP_BLOCK">{{ t('taskSubtype.NEW_GROUP_BLOCK') }}</option>
|
||||
<option value="NEW_FIT_RESERVATION">{{ t('taskSubtype.NEW_FIT_RESERVATION') }}</option>
|
||||
<option value="MANUAL_REVIEW">{{ t('taskSubtype.MANUAL_REVIEW') }}</option>
|
||||
<option value="S000">{{ t('taskSubtype.S000') }}</option>
|
||||
<option value="S999">{{ t('taskSubtype.S999') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -168,8 +171,14 @@
|
||||
<small>{{ formatReservationTaskTypeSummary(t, task.task_type, task.task_subtype) }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ task.display_order_key ?? task.temporary_order_no ?? task.order_id }}</strong>
|
||||
<small class="th-code">{{ task.order_id }}</small>
|
||||
<template v-if="isSourceMessageOnlyTask(task.task_type)">
|
||||
<strong>{{ t('taskList.sourceOnlyOrderLabel') }}</strong>
|
||||
<small>{{ t('taskList.hiddenTechnicalOrder') }}</small>
|
||||
</template>
|
||||
<template v-else>
|
||||
<strong>{{ task.display_order_key ?? task.temporary_order_no ?? task.order_id }}</strong>
|
||||
<small class="th-code">{{ task.order_id }}</small>
|
||||
</template>
|
||||
</td>
|
||||
<td>{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}</td>
|
||||
<td><ReservationStatusBadge :status="task.task_status" /></td>
|
||||
@@ -190,6 +199,7 @@
|
||||
{{ t('taskList.viewTask') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="!isSourceMessageOnlyTask(task.task_type)"
|
||||
class="row-action"
|
||||
:to="`/reservation/orders/${task.order_id}`"
|
||||
>
|
||||
@@ -264,6 +274,7 @@ import {
|
||||
formatReservationReadonlyReason,
|
||||
formatReservationTaskCard,
|
||||
formatReservationTaskTypeSummary,
|
||||
isReservationSourceMessageOnlyTask,
|
||||
} from '@/utils/reservationDisplay'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -357,6 +368,10 @@ function goToPage(page: number): void {
|
||||
filters.page_num = Math.min(Math.max(page, 1), totalPages.value)
|
||||
}
|
||||
|
||||
function isSourceMessageOnlyTask(taskType: string | null | undefined): boolean {
|
||||
return isReservationSourceMessageOnlyTask(taskType)
|
||||
}
|
||||
|
||||
function createPage(pageNum: number, pageSize: number, total: number): ReservationPageResult {
|
||||
return {
|
||||
page_num: pageNum,
|
||||
|
||||
@@ -136,6 +136,7 @@ POST /api/auth/logout
|
||||
- `display_order_key` 是前端优先展示的订单业务号或临时订单号;`group_code` 和 `confirmation_number` 只有在当前订单业务号类型匹配时返回。
|
||||
- 订单 ID、任务 ID、SourceMessage ID 在这些前端接口中按字符串返回,前端不要转换成 JavaScript number。
|
||||
- `SOURCE_MESSAGE_ONLY` 任务背后有隐藏技术订单用于满足后端任务外键,但订单列表不会返回该订单;任务列表中该类任务的 `display_order_key`、`temporary_order_no`、`group_code`、`confirmation_number` 可能为空,前端不要因此隐藏整条任务。
|
||||
- 当前前端已按 `SOURCE_MESSAGE_ONLY` 展示 S000/S999,只在任务列表和任务详情提供只读查看入口;`INFORMATIONAL_MESSAGE` 仅作为历史 Message Notification 兼容路径保留。
|
||||
- 任务列表里 `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S000/S999` 的记录只展示邮件来源和 SuperAgent 入口结果,不展示处理按钮。
|
||||
- 任务详情里 `source_message_only_result` 仅对 `SOURCE_MESSAGE_ONLY` 返回,包含 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id` 和 `raw_answer`;普通任务该字段为空。
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| P0 | 邮件会话详情接口 `GET /api/source-messages/{sourceMessageId}/conversation` | 邮件会话详情页 | 已完成第一版 |
|
||||
| 联调 | 演示数据 seed 接口 `POST /api/system/reservation/demo-data` | 本地 / test 前端页面看效果 | 已完成;仅 dev/test 受控使用 |
|
||||
| 联调 | Debug EML 上传接口 `POST /api/system/debug/eml-superagent-runs` | Debug 页面上传 `.eml` 看 SuperAgent 结果 | 已完成第一版;仅 dev/test 受控使用 |
|
||||
| P1 | S000/S999 特殊只读任务展示 | 任务列表、任务详情来源邮件查看 | 已完成后端第一版;通过任务列表 / 任务详情展示 `SOURCE_MESSAGE_ONLY`,不做独立 Message Notification 接口 |
|
||||
| P1 | S000/S999 特殊只读任务展示 | 任务列表、任务详情来源邮件查看 | 已完成后端第一版;前端已按 `SOURCE_MESSAGE_ONLY` 完成列表筛选、S000/S999 展示和详情只读展示,不做独立 Message Notification 接口 |
|
||||
| P1 | 任务卡前端字段白名单元数据接口 | 字段白名单调试、版本对齐 | 未完成;若任务详情已透出完整元数据,可后置 |
|
||||
| 后置 | 普通任务切换订单接口 | 任务详情订单归属调整 | 未完成;已确认后置 |
|
||||
|
||||
@@ -503,7 +503,7 @@ GET /api/reservation/tasks/{taskId}
|
||||
|
||||
## 9. S000/S999 特殊只读任务与历史 Message Notification
|
||||
|
||||
当前状态:后端不提供独立 Message Notification 列表 / 详情接口。SuperAgent 新入口返回 `S000,source_message_id` 或 `S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务,第一版前端通过 `GET /api/reservation/tasks?task_type=SOURCE_MESSAGE_ONLY` 和 `GET /api/reservation/tasks/{taskId}` 展示。
|
||||
当前状态:后端不提供独立 Message Notification 列表 / 详情接口。SuperAgent 新入口返回 `S000,source_message_id` 或 `S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务。前端已通过 `GET /api/reservation/tasks?task_type=SOURCE_MESSAGE_ONLY` 和 `GET /api/reservation/tasks/{taskId}` 展示,并在任务列表筛选中支持 `SOURCE_MESSAGE_ONLY`、`S000`、`S999`。
|
||||
|
||||
历史 `INFORMATIONAL_MESSAGE` 仍可通过任务列表 / 任务详情兼容展示,但新数据不要依赖它。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user