diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 9d9dae4..4be68c8 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -4,16 +4,16 @@ | --- | --- | | 最近更新 | 2026-07-21 | | 当前分支 | `feature/huangting` | -| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询、停止旧任务双写、Debug EML V4 profile 对齐、Room Information 后端展示模型与前端业务化展示、V4 任务详情 smoke 修复、Rooming List 确认自动 DEF 后端联动、Rooming List 前端轻量事项卡、Room Information 复核 pointer 与任务详情安全边界修复、Room Information 复核 pointer 运行时规则收口、复核 pointer 部署证明与运行时 trace、OWNER RATE Room Type / Rate Code 目录口径、Payment 附件预览后端安全摘要与前端预览接入、Trace 卡后端字段契约收口、Rooming List 事项确认卡文档口径,以及 V4 复核态卡片交互和字段白名单文档口径 | +| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询、停止旧任务双写、Debug EML V4 profile 对齐、Room Information 后端展示模型与前端业务化展示、V4 任务详情 smoke 修复、Rooming List 确认自动 DEF 后端联动、Rooming List 前端轻量事项卡、Room Information 复核 pointer 与任务详情安全边界修复、Room Information 复核 pointer 运行时规则收口、复核 pointer 部署证明与运行时 trace、OWNER RATE Room Type / Rate Code 目录口径、Payment 附件预览后端安全摘要与前端预览接入、Trace 卡后端字段契约收口、Trace 确认态字段刷新、Rooming List 事项确认卡文档口径、V4 复核态卡片交互和字段白名单文档口径,以及 V4 工作台 / 订单详情 / 任务详情普通酒店员工用户化展示契约与前端落地 | | 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;Debug EML V4 smoke 默认复用实时 AgentBus V4 Open API subject,避免误走历史 Debug V2/V3 profile。开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。Room Information 已完成后端稳定展示模型和前端业务化展示:`GET /api/reservation/order-tasks/{orderTaskId}` 在 `display_payload.room_information` 返回 New / Update / Cancel 的 `current_values`、`proposed_values`、`final_values`、`change_summary[]`,前端只消费该展示模型和 `fields[]`,不再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导;如果卡片 payload 已经是稳定 `room_information.final_values` 结构,后端会按稳定模型归一化查询和复核;Nights、Breakfast 和 Group Booking Status 均以后端派生值为准;确认和复核写入稳定 `confirmed_payload_json.room_information.final_values`,不回写 Agent 原始 `target_order`、Adult、邮件正文或附件 URL;接口对前端暴露的 `fields[].write_target` 使用 `confirmed_payload` / `review_resolution.field_overrides` 这类安全语义,不暴露内部列名;查询侧 `fields[].editable` 和命令侧 `review-resolution` 复核 pointer 校验已共用同一套 Room Information 字段策略。Trace 卡后端契约已收口:普通事项内容字段统一为 `trace_items[].text`,不使用 `content`;`department_code` 第一版只允许 `FO`、`HSK`、`FO+HSK`,任务详情字段会返回 `options_source=reservation_v4_trace_department_fixed` 和 `fixed_options[]` 三个固定选项;`EXTRA_BED.target_room_type_code` 只校验当前酒店 Room Type 目录存在,暂不校验当前订单已有房型;确认和复核共用同一套 Trace 字段白名单,`display_payload` / `confirmed_payload` 不返回 `target_order`、邮件正文、附件 URL、raw evidence 或 AI 原始 payload。复核 pointer 拒绝前会记录 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,包含 order task、card、incoming pointer、query-side editable pointers、command-side allowed pointers、validation error pointers 和 reject reason,但不记录 payload、邮件正文或附件 URL。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 和普通业务卡的展示 / 确认 payload 不再返回 Agent `target_order`,普通业务卡还会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload` 和 `confirmed_payload` 均以 DEF 后的确认快照为准;当前订单详情 `order_overview` 不返回 Group Booking Status 字段;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。V4 任务详情已支持 ROOMING_LIST 轻量事项卡:页面只显示 “Rooming List / 房表事项”、目标订单线索、人工处理说明和确认按钮,不展示 rows、名单明细、附件预览、导入 / 生成入口、AI payload、邮件正文或附件 URL;PENDING_CONFIRM 确认只提交 `version`,成功后完全使用后端刷新详情,不由前端自行设置 `group_booking_status=DEF`。OWNER RATE `RATECODE (2)` 已确认第一阶段 Room Type 稳定集合为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,不建立 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D 与 LIAN TAI 的 40 个规范化 Rate Code 仅作为当前酒店级 `RATE_CODE` 目录候选维护。Payment 卡已在 `display_payload.payment_attachments[]` 返回付款凭证附件安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否理论可预览 / 下载和可选 `external_media_id`,前端在 V4 任务详情 Payment 卡中按当前触发 SourceMessage 的 conversation 附件匹配缩略图、大图预览和非图片下载,匹配优先 `external_media_id` / `externalMediaId`,其次 `attachment_id`,不按文件名猜测;`attachment_ids[]` 第一版仍只读,前端不增删或替换附件集合,Payment 确认只提交 `version`,不提交附件 ID、附件 URL 或完整附件对象;真实 URL 仍只来自 SourceMessage 原文权限链路,权限不足或 conversation 失败时降级展示不可预览 / 不可下载。已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、OWNER RATE 目录导入、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | ## 1. 当前 Checkpoint -- 名称:`M002-V4-owner-rate-catalog-data-alignment` -- 状态:Done,本地已完成 OWNER RATE Room Type / Rate Code 固定种子、V25 数据收口脚本、V4 测试 fixture、文档同步和全量后端测试验证。 -- 目标:将 V4 Room Type 初始化 / 测试种子收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,将 Rate Code 初始化 / 测试种子收敛为 OWNER RATE `RATECODE (2)` 的 40 个规范化酒店级 code。 -- 边界:第一阶段不建立 Account -> Room Type / Rate Code 适用关系,不新增 lookup 过滤参数,不做价格计算、不改 Breakfast 派生规则、不接 PMS / OPERA / OHIP、不推进 M011。 -- 联调备注:前端继续通过 `GET /api/reservation/lookups/room-types` 和 `GET /api/reservation/lookups/rate-codes` 获取选项,不硬编码 OWNER RATE;测试 / 开发库若有人工旧目录 ACTIVE 项,可按目录设计文档第 16 节限定 SQL 停用。 +- 名称:`M002-V4-reservation-workflow-user-facing-frontend` +- 状态:Done,前端已把 V4 工作台 / 任务列表、订单详情页和订单任务详情页按普通酒店员工视角收口;默认页面不再作为 V4 模型 / Task Card 调试视图展示。 +- 目标:`/reservation/tasks` 展示为“待处理预订事项 / 预订事项”列表,普通筛选只保留全部、待处理、需要复核、已完成和来源通知;`/reservation/orders/{orderId}` 展示为订单总览页,突出当前确认快照、下一步处理、关联来源邮件、处理记录和事项状态摘要;`/reservation/order-tasks/{orderTaskId}` 展示为订单事项办理页,业务卡使用“预订基础信息、房型与日期、付款凭证、跟进事项、房表事项、来源邮件”等用户可理解文案。 +- 边界:本 checkpoint 不改后端接口、不改 V4 确认 / 复核 / Payment / Rooming List / Trace / Room Information 提交契约;技术字段继续用于路由、并发和提交,但默认主信息层级不展示 `V4`、`ORDER_TASK`、`SOURCE_NOTIFICATION`、Task Card、JSON Pointer、payload、内部状态码、version、route、order task ID、card ID、SourceMessage ID、adapter 诊断或附件 URL。 +- 联调备注:如后续需要更自然的用户标题、下一步动作短句、订单摘要短句、业务分类字段或调试区权限标记,建议后端补充 `display_title` / `next_step_label` / `summary_text` / debug visibility 标记,前端不要从 AI 原始 payload 自行拼装。 ## 2. 当前优先级 diff --git a/client/src/components/reservation/ReservationV4SourceMessageCard.vue b/client/src/components/reservation/ReservationV4SourceMessageCard.vue index 7158746..5c7b41f 100644 --- a/client/src/components/reservation/ReservationV4SourceMessageCard.vue +++ b/client/src/components/reservation/ReservationV4SourceMessageCard.vue @@ -5,17 +5,7 @@

{{ title }}

-

- {{ t('task.sourceMessageId') }}: {{ sourceMessageId }} -

-
@@ -45,11 +35,16 @@

{{ t('taskV4.sourceMessage.body') }}

- {{ t('taskV4.sourceMessage.bodyLoading') }} + {{ t('taskV4.sourceMessage.bodyCollapsed') }}

+

@@ -68,16 +63,8 @@ v-else class="source-body__text" > - {{ visibleMessageBodyText }} + {{ messageBodyText }}

-

{{ t('taskV4.sourceMessage.noBody') }}

+
@@ -125,7 +119,6 @@ import { computed, ref, watch } from 'vue' import { RouterLink } from 'vue-router' import { useI18n } from 'vue-i18n' -import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue' import { fetchSourceMessageConversation } from '@/services/reservationService' import type { ReservationRecord, @@ -145,7 +138,6 @@ const props = withDefaults(defineProps<{ }) const { t } = useI18n() -const bodyPreviewLength = 360 const currentMessage = ref(null) const bodyLoading = ref(false) const bodyError = ref(false) @@ -210,19 +202,8 @@ const messageBodyText = computed(() => { } return compactBodyText(currentMessage.value?.text_body ?? '') }) -const canToggleBody = computed(() => messageBodyText.value.length > bodyPreviewLength) -const visibleMessageBodyText = computed(() => { - if (!canToggleBody.value || bodyExpanded.value) { - return messageBodyText.value - } - return `${messageBodyText.value.slice(0, bodyPreviewLength).trimEnd()}...` -}) -const showExpandedHtmlBody = computed(() => - bodyExpanded.value && Boolean(sanitizedHtmlBody.value), -) - -watch(sourceMessageId, (nextSourceMessageId) => { - void loadCurrentSourceMessage(nextSourceMessageId) +watch(sourceMessageId, () => { + resetBodyState() }, { immediate: true }) async function loadCurrentSourceMessage(nextSourceMessageId: string | null) { @@ -230,7 +211,6 @@ async function loadCurrentSourceMessage(nextSourceMessageId: string | null) { const requestSequence = bodyRequestSequence currentMessage.value = null bodyError.value = false - bodyExpanded.value = false if (!nextSourceMessageId) { bodyLoading.value = false @@ -257,6 +237,21 @@ async function loadCurrentSourceMessage(nextSourceMessageId: string | null) { } } +function toggleBody(): void { + bodyExpanded.value = !bodyExpanded.value + if (bodyExpanded.value && !currentMessage.value && !bodyLoading.value) { + void loadCurrentSourceMessage(sourceMessageId.value) + } +} + +function resetBodyState(): void { + bodyRequestSequence += 1 + currentMessage.value = null + bodyError.value = false + bodyExpanded.value = false + bodyLoading.value = false +} + interface AttachmentSummary { key: string name: string diff --git a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue index cf39c86..dd8351d 100644 --- a/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue +++ b/client/src/components/reservation/ReservationV4TaskCardFieldRenderer.vue @@ -390,16 +390,6 @@ function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] { tone: 'warning', }) } - if (result.catalog_source || result.catalog_version) { - hints.push({ - key: `${kind}-meta`, - message: t('taskV4.lookup.catalogMeta', { - source: result.catalog_source ?? '-', - version: result.catalog_version ?? '-', - }), - tone: 'normal', - }) - } const invalidValue = invalidLookupValues[reservationV4FieldKey(field)] if (invalidValue) { hints.push({ diff --git a/client/src/components/reservation/ReservationV4TaskCardSection.vue b/client/src/components/reservation/ReservationV4TaskCardSection.vue index d6d4f3a..95eb8e0 100644 --- a/client/src/components/reservation/ReservationV4TaskCardSection.vue +++ b/client/src/components/reservation/ReservationV4TaskCardSection.vue @@ -8,30 +8,31 @@

{{ title }}

-

- #{{ card.card_id }} -

- +
+ + +
-
-
- {{ t('task.reviewStatus') }} - {{ card.review_status ?? '-' }} -
-
- {{ t('taskV4.readonlyReason') }} - {{ availabilityReason || '-' }} -
-
- {{ t('taskV4.blockedReason') }} - {{ card.availability.blocked_reason ?? '-' }} -
-
- {{ t('taskV4.orderTaskId') }} - {{ orderTaskId ?? '-' }} -
+
+

+ {{ notice }} +

-
- - {{ t('taskV4.displayPayload') }} - -
- -
-
-
-
- - - {{ t('task.viewOrder') }} - -
- @@ -196,10 +153,6 @@ import ReservationV4TaskCardFieldRenderer from '@/components/reservation/Reserva import ReservationV4TraceCard from '@/components/reservation/ReservationV4TraceCard.vue' import type { ReservationRecord, ReservationV4TaskCardResult } from '@/types/reservation' import { formatReservationReadonlyReason } from '@/utils/reservationDisplay' -import { - isReservationV4UnsafeDisplayKey, - stringifyReservationV4SafeDisplayValue, -} from '@/utils/reservationV4FieldRules' type ReviewForm = { confirmed_order_id: string @@ -214,7 +167,6 @@ const props = withDefaults(defineProps<{ editableKeys: string[] validationErrors?: Record hotelId?: string - orderTaskId?: string | null sourceMessageId?: string | null orderReference?: string | null targetLocator?: string | null @@ -229,7 +181,6 @@ const props = withDefaults(defineProps<{ }>(), { validationErrors: () => ({}), hotelId: undefined, - orderTaskId: null, sourceMessageId: null, orderReference: null, targetLocator: null, @@ -267,22 +218,17 @@ const availabilityReason = computed(() => { ? formatReservationReadonlyReason(t, props.card.availability.readonly_reason_code) : '') }) - -const safePayloadRows = computed(() => { - if (!props.card.display_payload) { - return [] - } - return Object.entries(props.card.display_payload) - .filter(([key]) => !isReservationV4UnsafeDisplayKey(key)) - .slice(0, 12) - .map(([key, value]) => ({ - key, - value: stringifyReservationV4SafeDisplayValue( - value, - t('taskV4.field.empty'), - t('taskV4.field.hidden'), - ), - })) +const showPrimaryAction = computed(() => + props.card.card_status === 'PENDING_CONFIRM' || props.card.card_status === 'REVIEW_REQUIRED', +) +const primaryActionDisabled = computed(() => + props.card.card_status === 'REVIEW_REQUIRED' ? !props.canReview : !props.canConfirm, +) +const showOrderLink = computed(() => Boolean(props.boundOrderId && !isRoomingListCard.value)) +const cardNotices = computed(() => { + return [ + availabilityReason.value, + ].filter((notice): notice is string => Boolean(notice?.trim())) }) function updateReviewForm(field: keyof ReviewForm, event: Event): void { @@ -297,6 +243,14 @@ function isPaymentAttachmentIdField(field: ReservationV4TaskCardResult['fields'] const normalizedKey = `${field.field_pointer || ''} ${field.field_path || ''}`.toLowerCase() return normalizedKey.includes('attachment_ids') } + +function handlePrimaryAction(): void { + if (props.card.card_status === 'REVIEW_REQUIRED') { + emit('resolveReview') + return + } + emit('confirm') +} diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 75c57d3..9d9e1ed 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -452,12 +452,12 @@ export default { order: { summary: 'Order summary', taskQueue: 'Order task queue', - v4TaskTimeline: 'V4 order task timeline', - v4TaskTimelineEmpty: 'No V4 order tasks.', - v4TaskTimelineHint: 'Shows safe order-task and card summaries only. Complete actions in the V4 task detail page.', - v4OverviewTitle: 'Current V4 confirmed snapshot', - v4OverviewSubtitle: 'Derived from confirmed cards only, not from unconfirmed AI suggestions.', - v4OverviewEmpty: 'No confirmed V4 order snapshot yet.', + v4TaskTimeline: 'Processing records', + v4TaskTimelineEmpty: 'No processing records.', + v4TaskTimelineHint: 'Shows source-email processing records and item status summaries only. Complete actions in the reservation item page.', + v4OverviewTitle: 'Order information overview', + v4OverviewSubtitle: 'Derived from confirmed items only, not from unconfirmed suggestions.', + v4OverviewEmpty: 'No confirmed order information yet.', accountCode: 'Account Code', accountName: 'Account Name', marketCode: 'Market Code', @@ -466,31 +466,36 @@ export default { rateCode: 'Rate Code', roomItems: 'Confirmed room items', latestConfirmedAt: 'Latest confirmed at', - traceCardStatus: 'Trace card status', - roomingListCardStatus: 'Rooming List card status', - paymentCardStatus: 'Payment card status', + traceCardStatus: 'Follow-up item status', + roomingListCardStatus: 'Rooming list item status', + paymentCardStatus: 'Payment voucher status', nextV4ActionTitle: 'Next action', - nextV4ActionSubtitle: 'The order overview only links to the task page; it does not confirm, review, or edit cards.', - nextV4ActionType: 'Action type', - nextV4ActionStatus: 'Action status', - nextV4CardId: 'Card ID', - openOrderTaskCount: 'Open V4 tasks', - enterV4Task: 'Open task page', - nextActionConfirm: 'Confirm card', - nextActionReview: 'Manual review', - nextActionNone: 'No pending action', - nextV4ActionEmpty: 'No next V4 action.', - relatedSourceMessages: 'Related source messages', + nextV4ActionSubtitle: 'The order overview only links to the handling page; it does not confirm, review, or edit items.', + nextV4ActionType: 'Next step', + nextV4ActionStatus: 'Handling status', + nextV4CardId: 'Item ID', + openOrderTaskCount: 'Open items', + enterV4Task: 'Open handling page', + nextActionConfirm: 'Confirm item', + nextActionReview: 'Needs review', + nextActionNone: 'No pending item', + nextStep: 'Reservation item', + nextStepConfirm: 'An item needs confirmation', + nextStepReview: 'An item needs review', + nextV4ActionEmpty: 'No next reservation item.', + relatedSourceMessages: 'Related source emails', relatedSourceMessagesHint: 'Shows headers and conversation summary only. Open the conversation page for full email content.', - relatedSourceMessagesEmpty: 'No related source messages.', + relatedSourceMessagesEmpty: 'No related source emails.', externalConversationId: 'External conversation ID', latestActivityAt: 'Latest activity', - v4CardsEmpty: 'No card summaries.', - cardId: 'Card ID', + v4CardsEmpty: 'No item status summaries.', + cardStatusSummary: 'Item status summary', + cardId: 'Item ID', reviewStatus: 'Review status', confirmedBy: 'Confirmed by', confirmedAt: 'Confirmed at', - sourceEventIndex: 'Source event {index}', + sourceEventIndex: 'Source email event {index}', + processingRecord: 'Source email processing record', sourceMessage: 'Source message', businessKeySource: 'Business key source', updatedAt: 'Last updated', @@ -508,25 +513,35 @@ export default { sourceSummaryPending: 'Source email summary fields for the order task timeline are pending backend support. Open a task for available source details.', }, taskList: { - title: 'Task list', - subtitle: 'Review Reservation work by task queue', + title: 'Pending reservation items', + subtitle: 'Review reservation items that need confirmation, review, or acknowledgement', keyword: 'Order / email keyword', keywordPlaceholder: 'Order no. / email keyword', taskType: 'Task type', taskSubtype: 'Task subtype', - tableTitle: 'Task data', - totalSuffix: 'tasks', + tableTitle: 'Reservation items', + totalSuffix: 'items', noSourceSubject: 'No source subject', sourceReceivedAt: 'Source time', + businessStatus: 'Item status', + filterPending: 'Pending', + filterReview: 'Needs review', + filterCompleted: 'Completed', + filterSourceNotification: 'Source notification', + noOpenItems: 'No reservation items need handling', + noPendingItems: 'No reservation items are pending confirmation', + noReviewItems: 'No reservation items need review', + noCompletedItems: 'No completed reservation items', + noSourceNotifications: 'No source notifications', itemType: 'Work item type', - itemTypeOrderTask: 'Order task', + itemTypeOrderTask: 'Reservation item', itemTypeSourceNotification: 'Source notification', orderTaskStatus: 'Order task status', cardStatus: 'Card status', orderUnresolved: 'Order ownership pending', sourceNotificationNoOrder: 'No linked order', - sourceNotificationNoOrderHint: 'S10/S99 read-only source notification', - cardCountsTotal: '{total} cards', + sourceNotificationNoOrderHint: 'This email is not linked to an order. Review the email content.', + cardCountsTotal: '{total} items', cardCountsPending: 'Pending {count}', cardCountsReview: 'Review {count}', cardCountsConfirmed: 'Confirmed {count}', @@ -537,42 +552,50 @@ export default { queueAll: 'All queue states', queueIncluded: 'In queue', queueExcluded: 'Not in queue', - viewTask: 'View task', + viewTask: 'Handle item', viewOrder: 'View order', viewConversation: 'View email conversation', sourceOnlyOrderLabel: 'Source message only', hiddenTechnicalOrder: 'No visible order', columns: { - item: 'Work item', + item: 'Item type', task: 'Task', order: 'Order', - card: 'Task card', + card: 'Item', status: 'Status', source: 'Source email', - cards: 'Cards', + cards: 'Item progress', queue: 'Queue', }, }, taskV4: { - title: 'V4 order task detail', - subtitle: 'Handle the current order task with the backend V4 task card model.', - orderTaskSummary: 'Order task summary', - orderTaskId: 'Order task ID', + title: 'Reservation item handling', + subtitle: 'Handle the current reservation item from the source email.', + orderTaskSummary: 'Item summary', + orderTaskId: 'Item ID', orderReference: 'Order reference', - targetBookingType: 'Target booking type', - targetLocator: 'Target locator', - targetResolutionStatus: 'Target resolution status', - cardCounts: 'Card counts', - sourceMessageCard: 'Source email card', - basicInformationCard: 'Basic information card', - businessCards: 'Business task cards', + targetBookingType: 'Booking type', + targetLocator: 'Order clue', + targetResolutionStatus: 'Order ownership', + cardCounts: 'Item counts', + sourceMessageCard: 'Source email', + basicInformationCard: 'Reservation basics', + roomInformationCard: 'Room and dates', + paymentCard: 'Payment voucher', + businessCards: 'Reservation items', adapterContractErrors: 'Adapter contract diagnostics', noAdapterContractErrors: 'No adapter contract errors.', - noBusinessCards: 'No business task cards.', + noBusinessCards: 'No reservation items to handle.', displayPayload: 'Display summary', rawCardPayload: 'Safe card summary', readonlyReason: 'Read-only reason', blockedReason: 'Blocked reason', + bookingTypeGroup: 'Group booking', + bookingTypeFit: 'FIT booking', + bookingTypeUnknown: 'Booking type pending', + targetResolved: 'Confirmed', + targetUnresolved: 'Pending confirmation', + targetPending: 'Pending confirmation', confirmCard: 'Confirm card', confirmingCard: 'Confirming', resolveReview: 'Submit review', @@ -624,14 +647,14 @@ export default { fileMeta: '{type} · {size}', }, roomingList: { - title: 'Rooming List action', + title: 'Rooming list item', targetOrder: 'Target order', unboundOrder: 'No visible order is bound yet', description: 'The current source message contains a Rooming List item that needs manual handling.', confirmHint: 'This first version only confirms that the item has been handled manually. Guest details and attachment content are not displayed or processed here.', }, trace: { - title: 'Trace notes', + title: 'Follow-up item', itemLabel: 'Item {index}', generalTitle: 'General note', extraBedTitle: 'Extra bed note', @@ -643,7 +666,6 @@ export default { stale: 'This catalog is a stale snapshot. You can still select from it; backend validation remains final.', warning: 'Catalog note: {warning}', error: 'Catalog is temporarily unavailable. Refresh and retry.', - catalogMeta: 'Catalog source: {source}, version: {version}', accountDerived: 'Market: {market} / Source: {source} will be shown from the catalog; backend confirmation remains final.', currentNotInCatalog: 'Original value "{value}" is not in the current catalog. Select again.', }, @@ -655,6 +677,7 @@ export default { attachments: 'Attachments', excerpt: 'Email excerpt', body: 'Current email body', + bodyCollapsed: 'The email body is folded. Expand to read the current trigger email.', bodyLoading: 'Loading current email body...', bodyLoadFailed: 'Current email body cannot be loaded right now. Open the email conversation to view it.', noBody: 'No current email body.', @@ -671,7 +694,7 @@ export default { selectPlaceholder: 'Select', }, error: { - loadFailed: 'Failed to load V4 order task detail.', + loadFailed: 'Failed to load the reservation item handling page.', actionFailed: 'Action failed. Check backend response details.', }, }, diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index a7d3989..fef3b6d 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -452,12 +452,12 @@ export default { order: { summary: 'สรุปออเดอร์', taskQueue: 'คิวงานของออเดอร์นี้', - v4TaskTimeline: 'ไทม์ไลน์งานออเดอร์ V4', - v4TaskTimelineEmpty: 'ยังไม่มีงานออเดอร์ V4', - v4TaskTimelineHint: 'แสดงเฉพาะสรุปงานและการ์ดที่ปลอดภัย หากต้องดำเนินการให้เข้าไปที่หน้ารายละเอียดงาน V4', - v4OverviewTitle: 'สแนปช็อต V4 ที่ยืนยันแล้ว', - v4OverviewSubtitle: 'มาจากการ์ดที่ยืนยันแล้วเท่านั้น ไม่ใช้คำแนะนำ AI ที่ยังไม่ยืนยัน', - v4OverviewEmpty: 'ยังไม่มีสแนปช็อตออเดอร์ V4 ที่ยืนยันแล้ว', + v4TaskTimeline: 'ประวัติการดำเนินการ', + v4TaskTimelineEmpty: 'ยังไม่มีประวัติการดำเนินการ', + v4TaskTimelineHint: 'แสดงเฉพาะประวัติการประมวลผลอีเมลต้นทางและสรุปสถานะรายการ หากต้องดำเนินการให้เข้าไปที่หน้าจัดการรายการจอง', + v4OverviewTitle: 'ภาพรวมข้อมูลออเดอร์', + v4OverviewSubtitle: 'มาจากรายการที่ยืนยันแล้วเท่านั้น ไม่ใช้ข้อมูลแนะนำที่ยังไม่ยืนยัน', + v4OverviewEmpty: 'ยังไม่มีข้อมูลออเดอร์ที่ยืนยันแล้ว', accountCode: 'Account Code', accountName: 'Account Name', marketCode: 'Market Code', @@ -466,31 +466,36 @@ export default { rateCode: 'Rate Code', roomItems: 'ประเภทห้องและจำนวนที่ยืนยันแล้ว', latestConfirmedAt: 'เวลายืนยันล่าสุด', - traceCardStatus: 'สถานะการ์ด Trace', - roomingListCardStatus: 'สถานะการ์ด Rooming List', - paymentCardStatus: 'สถานะการ์ด Payment', + traceCardStatus: 'สถานะรายการติดตาม', + roomingListCardStatus: 'สถานะรายการ Rooming List', + paymentCardStatus: 'สถานะหลักฐานการชำระเงิน', nextV4ActionTitle: 'ขั้นตอนถัดไป', - nextV4ActionSubtitle: 'หน้าสรุปออเดอร์เป็นทางเข้าเท่านั้น ไม่ยืนยัน ตรวจสอบ หรือแก้ไขการ์ดในหน้านี้', - nextV4ActionType: 'ประเภทการดำเนินการ', - nextV4ActionStatus: 'สถานะการดำเนินการ', - nextV4CardId: 'รหัสการ์ด', - openOrderTaskCount: 'งาน V4 ที่ยังไม่เสร็จ', - enterV4Task: 'ไปหน้าดำเนินการ', - nextActionConfirm: 'ยืนยันการ์ด', - nextActionReview: 'ตรวจสอบด้วยคน', - nextActionNone: 'ไม่มีงานรอดำเนินการ', - nextV4ActionEmpty: 'ยังไม่มีขั้นตอน V4 ถัดไป', - relatedSourceMessages: 'ข้อความต้นทางที่เกี่ยวข้อง', + nextV4ActionSubtitle: 'หน้าสรุปออเดอร์เป็นทางเข้าเท่านั้น ไม่ยืนยัน ตรวจสอบ หรือแก้ไขรายการในหน้านี้', + nextV4ActionType: 'ขั้นตอนถัดไป', + nextV4ActionStatus: 'สถานะการจัดการ', + nextV4CardId: 'รหัสรายการ', + openOrderTaskCount: 'รายการที่ยังต้องจัดการ', + enterV4Task: 'ไปหน้าจัดการ', + nextActionConfirm: 'ยืนยันรายการ', + nextActionReview: 'ต้องตรวจสอบ', + nextActionNone: 'ไม่มีรายการรอดำเนินการ', + nextStep: 'รายการจอง', + nextStepConfirm: 'มีรายการรอยืนยัน', + nextStepReview: 'มีรายการต้องตรวจสอบ', + nextV4ActionEmpty: 'ยังไม่มีรายการจองถัดไป', + relatedSourceMessages: 'อีเมลต้นทางที่เกี่ยวข้อง', relatedSourceMessagesHint: 'แสดงเฉพาะหัวอีเมลและสรุปเธรด หากต้องการอ่านทั้งหมดให้เปิดหน้าบทสนทนาอีเมล', - relatedSourceMessagesEmpty: 'ยังไม่มีข้อความต้นทางที่เกี่ยวข้อง', + relatedSourceMessagesEmpty: 'ยังไม่มีอีเมลต้นทางที่เกี่ยวข้อง', externalConversationId: 'รหัสเธรดภายนอก', latestActivityAt: 'กิจกรรมล่าสุด', - v4CardsEmpty: 'ยังไม่มีสรุปการ์ด', - cardId: 'รหัสการ์ด', + v4CardsEmpty: 'ยังไม่มีสรุปสถานะรายการ', + cardStatusSummary: 'สรุปสถานะรายการ', + cardId: 'รหัสรายการ', reviewStatus: 'สถานะตรวจสอบ', confirmedBy: 'ผู้ยืนยัน', confirmedAt: 'เวลายืนยัน', - sourceEventIndex: 'เหตุการณ์ต้นทาง {index}', + sourceEventIndex: 'เหตุการณ์อีเมลต้นทาง {index}', + processingRecord: 'ประวัติการประมวลผลอีเมลต้นทาง', sourceMessage: 'ข้อความต้นทาง', businessKeySource: 'แหล่งที่มาของเลขธุรกิจ', updatedAt: 'อัปเดตล่าสุด', @@ -508,25 +513,35 @@ export default { sourceSummaryPending: 'ข้อมูลสรุปอีเมลต้นทางในไทม์ไลน์งานยังรอหลังบ้านเพิ่มเติม โปรดเข้าไปดูจากงานที่เกี่ยวข้อง', }, taskList: { - title: 'รายการงาน', - subtitle: 'ดูคิวงานการจองตามงาน', + title: 'รายการจองที่ต้องจัดการ', + subtitle: 'ดูรายการจองที่ต้องยืนยัน ตรวจสอบ หรือรับทราบ', keyword: 'คำค้นออเดอร์ / อีเมล', keywordPlaceholder: 'เลขออเดอร์ / คำค้นอีเมล', taskType: 'ประเภทงาน', taskSubtype: 'ประเภทย่อยของงาน', - tableTitle: 'ข้อมูลงาน', - totalSuffix: 'งาน', + tableTitle: 'รายการจอง', + totalSuffix: 'รายการ', noSourceSubject: 'ไม่มีหัวข้ออีเมลต้นทาง', sourceReceivedAt: 'เวลารับอีเมล', + businessStatus: 'สถานะรายการ', + filterPending: 'รอดำเนินการ', + filterReview: 'ต้องตรวจสอบ', + filterCompleted: 'เสร็จแล้ว', + filterSourceNotification: 'แจ้งเตือนต้นทาง', + noOpenItems: 'ยังไม่มีรายการจองที่ต้องจัดการ', + noPendingItems: 'ยังไม่มีรายการจองที่รอยืนยัน', + noReviewItems: 'ยังไม่มีรายการจองที่ต้องตรวจสอบ', + noCompletedItems: 'ยังไม่มีรายการจองที่เสร็จแล้ว', + noSourceNotifications: 'ยังไม่มีแจ้งเตือนต้นทาง', itemType: 'ประเภทงานในคิว', - itemTypeOrderTask: 'งานออเดอร์', + itemTypeOrderTask: 'รายการจอง', itemTypeSourceNotification: 'แจ้งเตือนต้นทาง', orderTaskStatus: 'สถานะงานออเดอร์', cardStatus: 'สถานะการ์ด', orderUnresolved: 'ยังรอยืนยันออเดอร์', sourceNotificationNoOrder: 'ไม่ผูกกับออเดอร์', - sourceNotificationNoOrderHint: 'แจ้งเตือนต้นทาง S10/S99 แบบอ่านอย่างเดียว', - cardCountsTotal: 'รวม {total} การ์ด', + sourceNotificationNoOrderHint: 'อีเมลนี้ไม่ได้ผูกกับออเดอร์ โปรดตรวจสอบเนื้อหาอีเมล', + cardCountsTotal: 'รวม {total} รายการ', cardCountsPending: 'รอยืนยัน {count}', cardCountsReview: 'รอตรวจสอบ {count}', cardCountsConfirmed: 'ยืนยันแล้ว {count}', @@ -537,42 +552,50 @@ export default { queueAll: 'สถานะคิวทั้งหมด', queueIncluded: 'อยู่ในคิว', queueExcluded: 'ไม่อยู่ในคิว', - viewTask: 'ดูงาน', + viewTask: 'จัดการรายการ', viewOrder: 'ดูออเดอร์', viewConversation: 'ดูเธรดอีเมล', sourceOnlyOrderLabel: 'ข้อความต้นทางแบบอ่านอย่างเดียว', hiddenTechnicalOrder: 'ไม่มีออเดอร์ที่แสดงได้', columns: { - item: 'รายการงาน', + item: 'ประเภทรายการ', task: 'งาน', order: 'ออเดอร์', - card: 'การ์ดงาน', + card: 'รายการ', status: 'สถานะ', source: 'อีเมลต้นทาง', - cards: 'การ์ด', + cards: 'ความคืบหน้ารายการ', queue: 'คิว', }, }, taskV4: { - title: 'รายละเอียดงานออเดอร์ V4', - subtitle: 'จัดการงานออเดอร์ปัจจุบันตามโมเดลการ์ดงาน V4 จากหลังบ้าน', - orderTaskSummary: 'สรุปงานออเดอร์', - orderTaskId: 'รหัสงานออเดอร์', + title: 'จัดการรายการจอง', + subtitle: 'จัดการรายการจองปัจจุบันจากอีเมลต้นทาง', + orderTaskSummary: 'สรุปรายการ', + orderTaskId: 'รหัสรายการ', orderReference: 'อ้างอิงออเดอร์', - targetBookingType: 'ประเภทการจองเป้าหมาย', - targetLocator: 'ข้อมูลระบุเป้าหมาย', - targetResolutionStatus: 'สถานะการยืนยันเป้าหมาย', - cardCounts: 'จำนวนการ์ด', - sourceMessageCard: 'การ์ดอีเมลต้นทาง', - basicInformationCard: 'การ์ดข้อมูลพื้นฐาน', - businessCards: 'การ์ดงานธุรกิจ', + targetBookingType: 'ประเภทการจอง', + targetLocator: 'ข้อมูลช่วยระบุออเดอร์', + targetResolutionStatus: 'การผูกออเดอร์', + cardCounts: 'จำนวนรายการ', + sourceMessageCard: 'อีเมลต้นทาง', + basicInformationCard: 'ข้อมูลจองพื้นฐาน', + roomInformationCard: 'ห้องและวันที่', + paymentCard: 'หลักฐานการชำระเงิน', + businessCards: 'รายการจอง', adapterContractErrors: 'ข้อมูลวินิจฉัยสัญญา Adapter', noAdapterContractErrors: 'ไม่มีข้อผิดพลาดสัญญา Adapter', - noBusinessCards: 'ยังไม่มีการ์ดงานธุรกิจ', + noBusinessCards: 'ยังไม่มีรายการจองที่ต้องจัดการ', displayPayload: 'สรุปสำหรับแสดงผล', rawCardPayload: 'สรุปการ์ดที่ปลอดภัย', readonlyReason: 'เหตุผลที่แก้ไขไม่ได้', blockedReason: 'เหตุผลที่ถูกบล็อก', + bookingTypeGroup: 'จองแบบกรุ๊ป', + bookingTypeFit: 'จองแบบ FIT', + bookingTypeUnknown: 'รอยืนยันประเภทการจอง', + targetResolved: 'ยืนยันแล้ว', + targetUnresolved: 'รอยืนยัน', + targetPending: 'รอยืนยัน', confirmCard: 'ยืนยันการ์ด', confirmingCard: 'กำลังยืนยัน', resolveReview: 'ส่งผลตรวจสอบ', @@ -624,14 +647,14 @@ export default { fileMeta: '{type} · {size}', }, roomingList: { - title: 'งาน Rooming List', + title: 'รายการ Rooming List', targetOrder: 'ออเดอร์เป้าหมาย', unboundOrder: 'ยังไม่มีออเดอร์ที่มองเห็นได้ผูกอยู่', description: 'ข้อความต้นทางปัจจุบันมีรายการ Rooming List ที่ต้องให้เจ้าหน้าที่จัดการ', confirmHint: 'เวอร์ชันแรกนี้ใช้ยืนยันว่ารายการได้รับการจัดการด้วยคนแล้วเท่านั้น ไม่แสดงหรือประมวลผลรายชื่อแขกและเนื้อหาไฟล์แนบในหน้านี้', }, trace: { - title: 'หมายเหตุ Trace', + title: 'รายการติดตาม', itemLabel: 'รายการ {index}', generalTitle: 'หมายเหตุทั่วไป', extraBedTitle: 'รายการเตียงเสริม', @@ -643,7 +666,6 @@ export default { stale: 'แค็ตตาล็อกนี้เป็น snapshot เก่า ยังเลือกได้ แต่การตรวจสอบฝั่งหลังบ้านเป็นผลสุดท้าย', warning: 'หมายเหตุแค็ตตาล็อก: {warning}', error: 'แค็ตตาล็อกไม่พร้อมใช้งานชั่วคราว โปรดรีเฟรชแล้วลองใหม่', - catalogMeta: 'แหล่งที่มาแค็ตตาล็อก: {source}, เวอร์ชัน: {version}', accountDerived: 'จะแสดง Market: {market} / Source: {source} จากแค็ตตาล็อก โดยผลยืนยันจากหลังบ้านเป็นผลสุดท้าย', currentNotInCatalog: 'ค่าเดิม "{value}" ไม่อยู่ในแค็ตตาล็อกปัจจุบัน โปรดเลือกใหม่', }, @@ -655,6 +677,7 @@ export default { attachments: 'ไฟล์แนบ', excerpt: 'ข้อความบางส่วนของอีเมล', body: 'เนื้อหาอีเมลปัจจุบัน', + bodyCollapsed: 'เนื้อหาอีเมลถูกพับไว้ กดขยายเพื่ออ่านอีเมลต้นทางฉบับปัจจุบัน', bodyLoading: 'กำลังโหลดเนื้อหาอีเมลปัจจุบัน...', bodyLoadFailed: 'ยังโหลดเนื้อหาอีเมลปัจจุบันไม่ได้ โปรดเปิดเธรดอีเมลเพื่อดู', noBody: 'ไม่มีเนื้อหาอีเมลปัจจุบัน', @@ -671,7 +694,7 @@ export default { selectPlaceholder: 'เลือก', }, error: { - loadFailed: 'โหลดรายละเอียดงานออเดอร์ V4 ไม่สำเร็จ', + loadFailed: 'โหลดหน้าจัดการรายการจองไม่สำเร็จ', actionFailed: 'ดำเนินการไม่สำเร็จ โปรดตรวจข้อมูลตอบกลับจากหลังบ้าน', }, }, diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 59550ef..e769366 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -452,12 +452,12 @@ export default { order: { summary: '订单摘要', taskQueue: '同订单任务队列', - v4TaskTimeline: 'V4 订单任务时间线', - v4TaskTimelineEmpty: '暂无 V4 订单任务。', - v4TaskTimelineHint: '只展示订单任务和任务卡安全摘要;处理动作请进入 V4 任务详情页完成。', - v4OverviewTitle: 'V4 当前确认快照', - v4OverviewSubtitle: '仅来自已确认任务卡,不使用未确认 AI 建议。', - v4OverviewEmpty: '暂无已确认的 V4 订单快照。', + v4TaskTimeline: '处理记录', + v4TaskTimelineEmpty: '暂无处理记录。', + v4TaskTimelineHint: '只展示来源邮件处理记录和事项状态摘要;办理动作请进入订单事项办理页完成。', + v4OverviewTitle: '订单信息总览', + v4OverviewSubtitle: '仅来自已确认事项,不使用未确认建议。', + v4OverviewEmpty: '暂无已确认的订单信息。', accountCode: 'Account 代码', accountName: 'Account 名称', marketCode: 'Market 代码', @@ -466,31 +466,36 @@ export default { rateCode: 'Rate Code', roomItems: '已确认房型房量', latestConfirmedAt: '最近确认时间', - traceCardStatus: 'Trace 卡片状态', - roomingListCardStatus: 'Rooming List 卡片状态', - paymentCardStatus: 'Payment 卡片状态', + traceCardStatus: '跟进事项状态', + roomingListCardStatus: '房表事项状态', + paymentCardStatus: '付款凭证状态', nextV4ActionTitle: '下一步处理', - nextV4ActionSubtitle: '订单总览只提供入口,不直接确认、复核或编辑任务卡。', - nextV4ActionType: '动作类型', - nextV4ActionStatus: '动作状态', - nextV4CardId: '任务卡编号', - openOrderTaskCount: '未完成 V4 任务数', - enterV4Task: '进入处理页', - nextActionConfirm: '确认任务卡', - nextActionReview: '人工复核', - nextActionNone: '无待处理动作', - nextV4ActionEmpty: '暂无下一步 V4 处理动作。', - relatedSourceMessages: '关联来源消息', + nextV4ActionSubtitle: '订单总览只提供入口,不直接确认、复核或编辑事项。', + nextV4ActionType: '下一步', + nextV4ActionStatus: '处理状态', + nextV4CardId: '事项编号', + openOrderTaskCount: '待处理事项数', + enterV4Task: '进入办理页', + nextActionConfirm: '确认事项', + nextActionReview: '需要复核', + nextActionNone: '无待处理事项', + nextStep: '处理事项', + nextStepConfirm: '有事项待确认', + nextStepReview: '有事项需要复核', + nextV4ActionEmpty: '暂无下一步处理事项。', + relatedSourceMessages: '关联来源邮件', relatedSourceMessagesHint: '只展示邮件头和会话摘要;全文请进入邮件会话页查看。', - relatedSourceMessagesEmpty: '暂无关联来源消息。', + relatedSourceMessagesEmpty: '暂无关联来源邮件。', externalConversationId: '外部会话编号', latestActivityAt: '最近活动时间', - v4CardsEmpty: '暂无任务卡摘要。', - cardId: '任务卡编号', + v4CardsEmpty: '暂无事项状态摘要。', + cardStatusSummary: '事项状态摘要', + cardId: '事项编号', reviewStatus: '复核状态', confirmedBy: '确认人', confirmedAt: '确认时间', - sourceEventIndex: '来源事件 {index}', + sourceEventIndex: '来源邮件事件 {index}', + processingRecord: '来源邮件处理记录', sourceMessage: '来源消息', businessKeySource: '业务号来源', updatedAt: '最近更新', @@ -508,25 +513,35 @@ export default { sourceSummaryPending: '订单详情任务时间线的来源邮件摘要字段待后端补充;请从具体任务进入后查看。', }, taskList: { - title: '任务列表', - subtitle: '按任务查看预订待处理队列', + title: '待处理预订事项', + subtitle: '查看需要确认、复核或已读的预订事项', keyword: '订单 / 邮件关键词', keywordPlaceholder: '订单号 / 邮件关键词', taskType: '任务类型', taskSubtype: '任务细分', - tableTitle: '任务数据', - totalSuffix: '条任务', + tableTitle: '预订事项', + totalSuffix: '条事项', noSourceSubject: '暂无来源主题', sourceReceivedAt: '来源时间', + businessStatus: '事项状态', + filterPending: '待处理', + filterReview: '需要复核', + filterCompleted: '已完成', + filterSourceNotification: '来源通知', + noOpenItems: '暂无需要处理的预订事项', + noPendingItems: '暂无待确认的预订事项', + noReviewItems: '暂无需要复核的预订事项', + noCompletedItems: '暂无已完成的预订事项', + noSourceNotifications: '暂无来源通知', itemType: '工作项类型', - itemTypeOrderTask: '订单任务', + itemTypeOrderTask: '预订事项', itemTypeSourceNotification: '来源通知', orderTaskStatus: '订单任务状态', cardStatus: '卡片状态', orderUnresolved: '订单归属待确认', sourceNotificationNoOrder: '不关联订单', - sourceNotificationNoOrderHint: 'S10/S99 只读来源通知', - cardCountsTotal: '共 {total} 张卡', + sourceNotificationNoOrderHint: '该邮件不关联订单,请查看邮件内容。', + cardCountsTotal: '共 {total} 个事项', cardCountsPending: '待确认 {count}', cardCountsReview: '待复核 {count}', cardCountsConfirmed: '已确认 {count}', @@ -537,42 +552,50 @@ export default { queueAll: '全部队列状态', queueIncluded: '参与队列', queueExcluded: '不参与队列', - viewTask: '查看任务', + viewTask: '处理事项', viewOrder: '查看订单', viewConversation: '查看邮件会话', sourceOnlyOrderLabel: '来源消息只读', hiddenTechnicalOrder: '不关联可见订单', columns: { - item: '工作项', + item: '事项类型', task: '任务', order: '订单', - card: '任务卡', + card: '事项', status: '状态', source: '来源邮件', - cards: '卡片', + cards: '事项进度', queue: '队列', }, }, taskV4: { - title: 'V4 订单任务详情', - subtitle: '按后端 V4 任务卡模型处理当前订单任务', - orderTaskSummary: '订单任务摘要', - orderTaskId: '订单任务编号', + title: '订单事项办理', + subtitle: '按来源邮件办理当前订单事项', + orderTaskSummary: '事项摘要', + orderTaskId: '事项编号', orderReference: '订单引用', - targetBookingType: '目标预订类型', - targetLocator: '目标定位信息', - targetResolutionStatus: '归属确认状态', - cardCounts: '任务卡统计', - sourceMessageCard: '来源邮件卡', - basicInformationCard: '基础信息卡', - businessCards: '业务任务卡', + targetBookingType: '预订类型', + targetLocator: '订单线索', + targetResolutionStatus: '订单归属', + cardCounts: '事项统计', + sourceMessageCard: '来源邮件', + basicInformationCard: '预订基础信息', + roomInformationCard: '房型与日期', + paymentCard: '付款凭证', + businessCards: '预订事项', adapterContractErrors: '适配契约诊断', noAdapterContractErrors: '暂无适配契约异常。', - noBusinessCards: '暂无业务任务卡。', + noBusinessCards: '暂无需要办理的预订事项。', displayPayload: '展示摘要', rawCardPayload: '卡片安全摘要', readonlyReason: '不可操作原因', blockedReason: '阻塞原因', + bookingTypeGroup: '团队预订', + bookingTypeFit: '散客预订', + bookingTypeUnknown: '预订类型待确认', + targetResolved: '已确认', + targetUnresolved: '待确认', + targetPending: '待确认', confirmCard: '确认卡片', confirmingCard: '确认中', resolveReview: '提交复核', @@ -624,14 +647,14 @@ export default { fileMeta: '{type} · {size}', }, roomingList: { - title: 'Rooming List / 房表事项', + title: '房表事项', targetOrder: '目标订单', unboundOrder: '暂未绑定可见订单', description: '当前来源消息包含需要人工处理的 Rooming List 事项,请在酒店内部流程处理后确认该卡片。', confirmHint: '第一版仅确认该事项已由人工处理,不展示或处理名单明细和附件内容。', }, trace: { - title: 'Trace / 追踪备注事项', + title: '跟进事项', itemLabel: '事项 {index}', generalTitle: '普通事项', extraBedTitle: '加床事项', @@ -643,7 +666,6 @@ export default { stale: '当前目录为过期快照,仍可选择,最终以后端校验为准。', warning: '目录提示:{warning}', error: '目录暂不可用,请刷新后重试。', - catalogMeta: '目录来源:{source},版本:{version}', accountDerived: '选择后将参考目录展示 Market:{market} / Source:{source},最终以后端确认结果为准。', currentNotInCatalog: '原值“{value}”不在当前目录中,请重新选择。', }, @@ -655,6 +677,7 @@ export default { attachments: '附件', excerpt: '邮件片段', body: '当前邮件正文', + bodyCollapsed: '邮件正文已折叠,展开后读取当前触发邮件正文。', bodyLoading: '正在加载当前邮件正文...', bodyLoadFailed: '当前邮件正文暂时无法加载,可进入邮件会话查看。', noBody: '暂无当前邮件正文。', @@ -671,7 +694,7 @@ export default { selectPlaceholder: '请选择', }, error: { - loadFailed: 'V4 订单任务详情加载失败。', + loadFailed: '订单事项办理页加载失败。', actionFailed: '操作失败,请检查后端返回信息。', }, }, diff --git a/client/src/tests/reservationService.spec.ts b/client/src/tests/reservationService.spec.ts index ba31da8..6530a5a 100644 --- a/client/src/tests/reservationService.spec.ts +++ b/client/src/tests/reservationService.spec.ts @@ -248,7 +248,7 @@ describe('reservationService real API mode', () => { page_size: 20, }) await fetchReservationV4RateCodeLookups({ - keyword: 'BAR', + keyword: 'GRPA2-850UP', page_num: 1, page_size: 20, }) @@ -265,7 +265,7 @@ describe('reservationService real API mode', () => { ) expect(fetchMock).toHaveBeenNthCalledWith( 3, - '/api/reservation/lookups/rate-codes?hotel_id=HOTEL-BKK&keyword=BAR&page_num=1&page_size=20', + '/api/reservation/lookups/rate-codes?hotel_id=HOTEL-BKK&keyword=GRPA2-850UP&page_num=1&page_size=20', expect.objectContaining({ method: 'GET' }), ) }) @@ -336,7 +336,7 @@ describe('reservationService real API mode', () => { field_overrides: [ { field_pointer: '/business_fields/room_items/0/room_type_code', - value: 'TWN', + value: 'RM2', }, ], }) @@ -357,7 +357,7 @@ describe('reservationService real API mode', () => { field_overrides: [ { field_pointer: '/business_fields/room_items/0/room_type_code', - value: 'TWN', + value: 'RM2', }, ], }), diff --git a/client/src/tests/reservationV4FieldRules.spec.ts b/client/src/tests/reservationV4FieldRules.spec.ts index a7059b2..f24005e 100644 --- a/client/src/tests/reservationV4FieldRules.spec.ts +++ b/client/src/tests/reservationV4FieldRules.spec.ts @@ -37,12 +37,12 @@ describe('reservationV4FieldRules', () => { it('builds initial values from backend fields using field pointers', () => { const fields = [ createV4Field('/basic_information/account_code', { value: 'HANATOUR' }), - createV4Field('/business_fields/after/room_items/0/room_type_code', { value: 'TWN' }), + createV4Field('/business_fields/after/room_items/0/room_type_code', { value: 'RM2' }), ] expect(buildReservationV4InitialFieldValues(fields)).toEqual({ '/basic_information/account_code': 'HANATOUR', - '/business_fields/after/room_items/0/room_type_code': 'TWN', + '/business_fields/after/room_items/0/room_type_code': 'RM2', }) }) @@ -55,7 +55,7 @@ describe('reservationV4FieldRules', () => { raw_readonly: true, }), createV4Field('/business_fields/after/room_items/0/room_type_code', { - value: 'KING', + value: 'SU1', edit_scope: 'MANUAL_REVIEW_ONLY', write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES', }), @@ -64,7 +64,7 @@ describe('reservationV4FieldRules', () => { const payload = buildReservationV4ConfirmedPayload(fields, { '/basic_information/account_code': 'HANATOUR', '/basic_information/read_only_label': 'SHOULD_NOT_SEND', - '/business_fields/after/room_items/0/room_type_code': 'TWN', + '/business_fields/after/room_items/0/room_type_code': 'RM2', }) expect(payload).toEqual({ @@ -82,19 +82,19 @@ describe('reservationV4FieldRules', () => { write_target: 'REVIEW_RESOLUTION_FIELD_OVERRIDES', }), createV4Field('/room_information/final_values/room_items/0/room_type_raw', { - value: 'TWN', + value: 'RM2', editable: false, write_target: 'NONE', }), ] expect(buildReservationV4ReviewOverrides(fields, { - '/room_information/final_values/room_items/0/room_type_code': 'TWN', + '/room_information/final_values/room_items/0/room_type_code': 'RM2', '/room_information/final_values/room_items/0/room_type_raw': 'Twin', })).toEqual([ { field_pointer: '/room_information/final_values/room_items/0/room_type_code', - value: 'TWN', + value: 'RM2', }, ]) }) @@ -111,10 +111,10 @@ describe('reservationV4FieldRules', () => { expect(isReservationV4ConfirmWritableField(confirmField)).toBe(false) expect(isReservationV4ReviewWritableField(reviewField)).toBe(false) expect(buildReservationV4ConfirmedPayload([confirmField], { - '/business_fields/after/room_items/0/rate_code': 'BAR', + '/business_fields/after/room_items/0/rate_code': 'GRPA2-850UP', })).toEqual({}) expect(buildReservationV4ReviewOverrides([reviewField], { - '/business_fields/after/room_items/0/room_type_code': 'TWN', + '/business_fields/after/room_items/0/room_type_code': 'RM2', })).toEqual([]) }) @@ -184,7 +184,7 @@ describe('reservationV4FieldRules', () => { expect(buildReservationV4ReviewOverrides(fields, { '/source_message/subject': 'SHOULD_NOT_SEND', '/room_information/final_values/arrival_date': '2026-07-26', - '/business_fields/after/room_items/0/room_type_code': 'TWN', + '/business_fields/after/room_items/0/room_type_code': 'RM2', })).toEqual([ { field_pointer: '/room_information/final_values/arrival_date', @@ -192,7 +192,7 @@ describe('reservationV4FieldRules', () => { }, { field_pointer: '/business_fields/after/room_items/0/room_type_code', - value: 'TWN', + value: 'RM2', }, ]) }) @@ -203,7 +203,7 @@ describe('reservationV4FieldRules', () => { group_code: 'GRP-001', raw_evidence: 'raw email body', nested: { - room_type_code: 'TWN', + room_type_code: 'RM2', oss_url: 'https://oss.example/private-file.pdf', }, safe_url_value: 'https://oss.example/also-hidden.pdf', @@ -211,7 +211,7 @@ describe('reservationV4FieldRules', () => { }, 'empty', 'hidden') expect(text).toContain('GRP-001') - expect(text).toContain('TWN') + expect(text).toContain('RM2') expect(text).not.toContain('raw email body') expect(text).not.toContain('https://oss.example') expect(text).not.toContain('oss_url') diff --git a/client/src/tests/reservationV4Views.spec.ts b/client/src/tests/reservationV4Views.spec.ts index d136cdb..288836a 100644 --- a/client/src/tests/reservationV4Views.spec.ts +++ b/client/src/tests/reservationV4Views.spec.ts @@ -52,7 +52,7 @@ describe('reservation V4 pages', () => { mockCatalogLookups() }) - it('renders V4 order task cards, loads lookup options and confirms only selected codes', async () => { + it('renders reservation handling cards, hides default diagnostics and confirms only selected codes', async () => { const detail = createOrderTaskDetail() vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail) vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({ @@ -69,11 +69,18 @@ describe('reservation V4 pages', () => { ) await flushPromises() - expect(wrapper.text()).toContain('V4 订单任务详情') + expect(wrapper.text()).toContain('订单事项办理') expect(wrapper.text()).toContain('Booking Request') - expect(wrapper.text()).toContain('基础信息卡') - expect(wrapper.text()).toContain('业务任务卡') - expect(wrapper.text()).toContain('Backend contract issue') + expect(wrapper.text()).toContain('预订基础信息') + expect(wrapper.text()).toContain('预订事项') + expect(wrapper.text()).toContain('房型与日期') + expect(wrapper.text()).not.toContain('V4 订单任务详情') + expect(wrapper.text()).not.toContain('Backend contract issue') + expect(wrapper.text()).not.toContain('card-basic') + expect(wrapper.text()).not.toContain('card-room') + expect(wrapper.text()).not.toContain('#9001') + expect(wrapper.text()).not.toContain(zhCN.taskV4.orderTaskId) + expect(wrapper.text()).not.toContain(zhCN.taskV4.displayPayload) expect(service.fetchReservationV4AccountLookups).toHaveBeenCalledWith({ hotel_id: 'HOTEL-TEST', page_num: 1, @@ -102,7 +109,7 @@ describe('reservation V4 pages', () => { }) }) - it('keeps V4 task card layout classes for styled card metadata and payload summary', async () => { + it('keeps task card layout classes and places the primary card action in the card header', async () => { const detail = createOrderTaskDetail() detail.business_cards[0]!.card_type = 'VOUCHER' detail.business_cards[0]!.display_payload = { @@ -119,10 +126,10 @@ describe('reservation V4 pages', () => { expect(wrapper.findAll('.task-card-section')).toHaveLength(2) expect(wrapper.find('.task-card-section__header').exists()).toBe(true) - expect(wrapper.find('.card-meta-grid').exists()).toBe(true) - expect(wrapper.find('.card-meta-item').text()).toContain(zhCN.task.reviewStatus) - expect(wrapper.find('details.safe-payload > summary').text()).toContain(zhCN.taskV4.displayPayload) - expect(wrapper.find('.card-actions .primary-button').text()).toContain(zhCN.taskV4.confirmCard) + expect(wrapper.find('.card-meta-grid').exists()).toBe(false) + expect(wrapper.find('details.safe-payload').exists()).toBe(false) + expect(wrapper.find('.task-card-section__header .primary-button').text()).toContain(zhCN.taskV4.confirmCard) + expect(wrapper.find('.card-actions .primary-button').exists()).toBe(false) }) it('does not render direct attachment URLs from V4 source message payload', async () => { @@ -184,17 +191,16 @@ describe('reservation V4 pages', () => { ) await flushPromises() - expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001') const pageText = wrapper.text() const basicIndex = pageText.indexOf(zhCN.taskV4.basicInformationCard) const businessIndex = pageText.indexOf(zhCN.taskV4.businessCards) - const sourceIndex = pageText.indexOf(zhCN.taskV4.sourceMessageCard) expect(basicIndex).toBeGreaterThanOrEqual(0) expect(businessIndex).toBeGreaterThan(basicIndex) - expect(sourceIndex).toBeGreaterThan(businessIndex) const sections = wrapper.findAll('.th-section') expect(sections[sections.length - 1]?.text()).toContain(zhCN.taskV4.sourceMessageCard) - expect(pageText).toContain('Current trigger email body opening.') + expect(sections[sections.length - 1]?.text()).toContain(zhCN.taskV4.sourceMessage.bodyCollapsed) + expect(service.fetchSourceMessageConversation).not.toHaveBeenCalled() + expect(pageText).not.toContain('Current trigger email body opening.') expect(pageText).not.toContain('OLDER_MESSAGE_BODY') expect(pageText).not.toContain('REPLY_MESSAGE_BODY') expect(pageText).not.toContain('CURRENT_BODY_TAIL') @@ -209,6 +215,8 @@ describe('reservation V4 pages', () => { await expandButton!.trigger('click') await flushPromises() + expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001') + expect(wrapper.text()).toContain('Current trigger email body opening.') expect(wrapper.text()).toContain('CURRENT_BODY_TAIL') expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessage.collapseBody) }) @@ -228,6 +236,17 @@ describe('reservation V4 pages', () => { await flushPromises() expect(wrapper.text()).toContain('Please book one twin room.') + expect(wrapper.text()).not.toContain(zhCN.taskV4.sourceMessage.bodyLoadFailed) + expect(service.fetchSourceMessageConversation).not.toHaveBeenCalled() + + const expandButton = wrapper + .findAll('button') + .find((button) => button.text().includes(zhCN.taskV4.sourceMessage.showFullBody)) + expect(expandButton).toBeTruthy() + await expandButton!.trigger('click') + await flushPromises() + + expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001') expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessage.bodyLoadFailed) expect(wrapper.text()).toContain(zhCN.task.viewConversation) }) @@ -250,7 +269,7 @@ describe('reservation V4 pages', () => { ) await flushPromises() - expect(wrapper.text()).toContain('Sanitized current email opening.') + expect(wrapper.text()).not.toContain('Sanitized current email opening.') expect(wrapper.text()).not.toContain('SANITIZED_HTML_TAIL') expect(wrapper.text()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER') @@ -261,6 +280,7 @@ describe('reservation V4 pages', () => { await expandButton!.trigger('click') await flushPromises() + expect(wrapper.text()).toContain('Sanitized current email opening.') expect(wrapper.text()).toContain('SANITIZED_HTML_TAIL') expect(wrapper.html()).toContain('

Sanitized current email opening.') expect(wrapper.html()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER') @@ -283,6 +303,14 @@ describe('reservation V4 pages', () => { ) await flushPromises() + expect(wrapper.text()).not.toContain('Plain text fallback body.') + const expandButton = wrapper + .findAll('button') + .find((button) => button.text().includes(zhCN.taskV4.sourceMessage.showFullBody)) + expect(expandButton).toBeTruthy() + await expandButton!.trigger('click') + await flushPromises() + expect(wrapper.text()).toContain('Plain text fallback body.') expect(wrapper.text()).not.toContain('SANITIZED_HTML_SHOULD_NOT_RENDER') expect(wrapper.text()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER') @@ -373,7 +401,7 @@ describe('reservation V4 pages', () => { const paymentCard = wrapper .findAll('.task-card-section') - .find((section) => section.text().includes(zhCN.cardType.PAYMENT)) + .find((section) => section.text().includes(zhCN.taskV4.paymentCard)) expect(paymentCard).toBeTruthy() await paymentCard!.find('button.primary-button').trigger('click') await flushPromises() @@ -409,9 +437,8 @@ describe('reservation V4 pages', () => { expect(roomingListCard.text()).toContain('GRP-001') expect(roomingListSection!.findAll('a').filter((link) => link.text() === zhCN.task.viewOrder)).toHaveLength(1) expect(wrapper.text()).toContain(zhCN.taskV4.sourceMessageCard) - expect(wrapper.text().indexOf(zhCN.taskV4.sourceMessageCard)).toBeGreaterThan( - wrapper.text().indexOf(zhCN.taskV4.roomingList.title), - ) + const sections = wrapper.findAll('.th-section') + expect(sections[sections.length - 1]?.text()).toContain(zhCN.taskV4.sourceMessageCard) expect(roomingListSection!.find('details.safe-payload').exists()).toBe(false) expect(roomingListSection!.text()).not.toContain('ROW_SHOULD_NOT_RENDER') @@ -592,13 +619,13 @@ describe('reservation V4 pages', () => { .find((section) => section.find('[data-testid="trace-card"]').exists()) expect(traceSection).toBeTruthy() expect(traceSection!.text()).toContain('SET EXTRA BED') - expect(traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').text()).toContain('TWN - Twin') + expect(traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').text()).toContain('RM2') const roomCountInput = traceSection!.find('input[name="/trace_items/0/extra_bed_room_count"]') expect(roomCountInput.attributes('type')).toBe('number') expect(roomCountInput.attributes('min')).toBe('1') expect(roomCountInput.attributes('step')).toBe('1') - await traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').setValue('TWN') + await traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').setValue('RM2') await roomCountInput.setValue('0') await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('FO+HSK') await traceSection!.find('button.primary-button').trigger('click') @@ -616,7 +643,7 @@ describe('reservation V4 pages', () => { confirmed_payload: { trace_items: [ { - target_room_type_code: 'TWN', + target_room_type_code: 'RM2', extra_bed_room_count: 2, department_code: 'FO+HSK', }, @@ -770,11 +797,11 @@ describe('reservation V4 pages', () => { group_block_name: '前端修正团队名', arrival_date: '2026-07-26', departure_date: '2026-07-29', - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', group_booking_status: 'DEF', room_items: [ { - room_type_code: 'TWN', + room_type_code: 'RM2', room_count: 2, }, ], @@ -858,13 +885,13 @@ describe('reservation V4 pages', () => { arrival_date: '2026-07-26', departure_date: '2026-07-29', nights: 3, - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', breakfast_included: true, group_booking_status: 'TEN', group_booking_status_label: 'TEN-Tentative', room_items: [ { - room_type_code: 'TWN', + room_type_code: 'RM2', room_count: 2, }, ], @@ -874,7 +901,7 @@ describe('reservation V4 pages', () => { departure_date: '2026-07-31', room_items: [ { - room_type_code: 'DBL', + room_type_code: 'RM3', room_count: 3, }, ], @@ -884,13 +911,13 @@ describe('reservation V4 pages', () => { arrival_date: '2026-07-27', departure_date: '2026-07-31', nights: 4, - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', breakfast_included: true, group_booking_status: 'TEN', group_booking_status_label: 'TEN-Tentative', room_items: [ { - room_type_code: 'DBL', + room_type_code: 'RM3', room_count: 3, }, ], @@ -953,13 +980,13 @@ describe('reservation V4 pages', () => { arrival_date: '2026-08-01', departure_date: '2026-08-05', nights: 4, - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', breakfast_included: true, group_booking_status: 'DEF', group_booking_status_label: 'DEF-Definite', room_items: [ { - room_type_code: 'KING', + room_type_code: 'SU1', room_count: 1, }, ], @@ -969,13 +996,13 @@ describe('reservation V4 pages', () => { arrival_date: '2026-08-01', departure_date: '2026-08-05', nights: 4, - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', breakfast_included: true, group_booking_status: 'DEF', group_booking_status_label: 'DEF-Definite', room_items: [ { - room_type_code: 'KING', + room_type_code: 'SU1', room_count: 1, }, ], @@ -994,7 +1021,7 @@ describe('reservation V4 pages', () => { expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.currentValues) expect(roomCard.text()).toContain(zhCN.taskV4.roomInformation.finalValues) expect(roomCard.text()).toContain('GRP-V4-RI-CANCEL-001') - expect(roomCard.text()).toContain('KING') + expect(roomCard.text()).toContain('SU1') expect(roomCard.find('input[name="/room_information/final_values/arrival_date"]').exists()).toBe(false) expect(roomCard.find('select[name="/room_information/final_values/group_booking_status"]').exists()).toBe(false) }) @@ -1027,7 +1054,7 @@ describe('reservation V4 pages', () => { await flushPromises() const roomCard = wrapper.find('[data-testid="room-information-card"]') - await roomCard.find('select[name="/room_information/final_values/room_items/0/room_type_code"]').setValue('TWN') + await roomCard.find('select[name="/room_information/final_values/room_items/0/room_type_code"]').setValue('RM2') await wrapper.find('input[name="v4_confirmed_order_id"]').setValue('order-2001') await wrapper.find('textarea').setValue('confirmed by email evidence') await roomCard.find('button').trigger('click') @@ -1040,7 +1067,7 @@ describe('reservation V4 pages', () => { field_overrides: [ { field_pointer: '/room_information/final_values/room_items/0/room_type_code', - value: 'TWN', + value: 'RM2', }, ], }) @@ -1073,8 +1100,8 @@ describe('reservation V4 pages', () => { expect(wrapper.text()).toContain(zhCN.taskV4.lookup.empty) expect(wrapper.text()).toContain(zhCN.taskV4.lookup.stale) expect(wrapper.text()).toContain('PMS sync is stale') - expect(wrapper.text()).toContain('PMS_SYNC') - expect(wrapper.text()).toContain('stale-v1') + expect(wrapper.text()).not.toContain('PMS_SYNC') + expect(wrapper.text()).not.toContain('stale-v1') }) it('does not submit a catalog field value that is missing from the loaded lookup result', async () => { @@ -1359,8 +1386,8 @@ function mockCatalogLookups() { stale: false, items: [ { - code: 'TWN', - display_name: 'Twin', + code: 'RM2', + display_name: 'RM2', status: 'ACTIVE', catalog_source: 'SYSTEM_MANAGED', market_code: null, @@ -1386,8 +1413,8 @@ function mockCatalogLookups() { stale: false, items: [ { - code: 'GROUP', - display_name: 'Group Rate', + code: 'GRPA2-850UP', + display_name: 'GRPA2-850UP', status: 'ACTIVE', catalog_source: 'SYSTEM_MANAGED', market_code: null, @@ -1750,7 +1777,7 @@ function useTraceBusinessCard( itemType === 'EXTRA_BED' ? { item_type: 'EXTRA_BED', - target_room_type_code: 'TWN', + target_room_type_code: 'RM2', extra_bed_room_count: 1, department_code: 'FO', content: 'CONTENT_SHOULD_NOT_RENDER', @@ -1770,7 +1797,7 @@ function useTraceBusinessCard( ? [ createField('/trace_items/0/target_room_type_code', { display_name: 'Target Room Type', - value: 'TWN', + value: 'RM2', options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG', control_type: 'SELECT', edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM', @@ -1891,13 +1918,13 @@ function createRoomInformationDisplayPayload(overrides: { arrival_date: '2026-07-26', departure_date: '2026-07-29', nights: 3, - rate_code: 'GROUP', + rate_code: 'GRPA2-850UP', breakfast_included: true, group_booking_status: 'TEN', group_booking_status_label: 'TEN-Tentative', room_items: [ { - room_type_code: 'TWN', + room_type_code: 'RM2', room_count: 2, }, ], @@ -1962,7 +1989,7 @@ function createRoomInformationFields(reviewMode = false): ReservationV4TaskCardR }), createField('/room_information/final_values/rate_code', { display_name: 'Rate Code', - value: 'GROUP', + value: 'GRPA2-850UP', required: true, control_type: 'SELECT', options_source: 'RESERVATION_V4_RATE_CODE_CATALOG', @@ -1972,7 +1999,7 @@ function createRoomInformationFields(reviewMode = false): ReservationV4TaskCardR }), createField('/room_information/final_values/room_items/0/room_type_code', { display_name: '房型代码', - value: reviewMode ? '' : 'TWN', + value: reviewMode ? '' : 'RM2', required: true, control_type: 'SELECT', options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG', diff --git a/client/src/tests/reservationViews.spec.ts b/client/src/tests/reservationViews.spec.ts index 651ee8a..097ae2d 100644 --- a/client/src/tests/reservationViews.spec.ts +++ b/client/src/tests/reservationViews.spec.ts @@ -570,32 +570,36 @@ describe('reservation P0 views', () => { vi.mocked(service.fetchSourceMessageConversation).mockReset() }) - it('renders the V4 order overview, next action, related source messages and safe card timeline', async () => { + it('renders the user-facing order overview, next action, related source emails and processing records', async () => { vi.mocked(service.fetchReservationOrderDetail).mockResolvedValue(createOrderDetailResult()) const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001') await vi.dynamicImportSettled() - expect(wrapper.text()).toContain('V4 当前确认快照') + expect(wrapper.text()).toContain('订单信息总览') expect(wrapper.text()).toContain('QBD_TRAVEL') expect(wrapper.text()).toContain('Q.B.D. TRAVEL GROUP CO., LTD') expect(wrapper.text()).toContain('2026-07-26') expect(wrapper.text()).toContain('2026-07-29') expect(wrapper.text()).toContain('RM1 × 2') expect(wrapper.text()).toContain('下一步处理') - expect(wrapper.find('a[href="/reservation/order-tasks/9001"]').text()).toContain('进入处理页') + expect(wrapper.find('a[href="/reservation/order-tasks/9001"]').text()).toContain('进入办理页') expect(wrapper.find('.primary-link[href="/reservation/order-tasks/9001"]').exists()).toBe(true) - expect(wrapper.text()).toContain('关联来源消息') + expect(wrapper.text()).toContain('关联来源邮件') expect(wrapper.text()).toContain('v4@example.test') expect(wrapper.text()).toContain('查看邮件会话') expect(wrapper.find('a[href="/reservation/source-messages/30003/conversation"]').exists()).toBe(true) - expect(wrapper.text()).toContain(zhCN.order.v4TaskTimeline) - expect(wrapper.text()).toContain('order-ref-1') + expect(wrapper.text()).toContain('处理记录') + expect(wrapper.text()).not.toContain('order-ref-1') expect(wrapper.text()).toContain('V4 Booking Package') expect(wrapper.find('.v4-timeline-item__main[href="/reservation/order-tasks/9001"]').exists()).toBe(true) - expect(wrapper.text()).toContain('基础信息卡') + expect(wrapper.text()).toContain('事项状态摘要') + expect(wrapper.text()).toContain('预订基础信息') expect(wrapper.text()).toContain('frontend-query-admin') expect(wrapper.text()).toContain('待复核') + expect(wrapper.text()).not.toContain('V4 当前确认快照') + expect(wrapper.text()).not.toContain('任务卡编号') + expect(wrapper.text()).not.toContain('card-basic') expect(wrapper.text()).not.toContain('unsafe ai payload must stay hidden') expect(wrapper.text()).not.toContain('unsafe display payload must stay hidden') expect(wrapper.text()).not.toContain('unsafe confirmed payload must stay hidden') @@ -622,9 +626,9 @@ describe('reservation P0 views', () => { const wrapper = await mountWithPlugins(ReservationOrderDetailView, '/reservation/orders/20001') await vi.dynamicImportSettled() - expect(wrapper.text()).toContain('暂无已确认的 V4 订单快照') - expect(wrapper.text()).toContain('暂无下一步 V4 处理动作') - expect(wrapper.text()).toContain('暂无关联来源消息') + expect(wrapper.text()).toContain('暂无已确认的订单信息') + expect(wrapper.text()).toContain('暂无下一步处理事项') + expect(wrapper.text()).toContain('暂无关联来源邮件') expect(wrapper.text()).toContain(zhCN.order.v4TaskTimelineEmpty) expect(wrapper.find('a[href^="/reservation/order-tasks/"]').exists()).toBe(false) }) @@ -830,10 +834,11 @@ describe('reservation P0 views', () => { expect(wrapper.text()).toContain('GRP-001') expect(wrapper.text()).toContain('Booking Request') - expect(wrapper.text()).toContain('订单任务') + expect(wrapper.text()).toContain('预订事项') expect(wrapper.text()).toContain('待确认 1') expect(wrapper.text()).toContain('待复核 1') expect(wrapper.text()).toContain('查看邮件会话') + expect(wrapper.text()).not.toContain('#9001') expect(wrapper.text()).not.toContain('接口待补') expect(service.fetchReservationV4WorkbenchItems).toHaveBeenCalledWith( expect.objectContaining({ @@ -866,10 +871,12 @@ describe('reservation P0 views', () => { expect(wrapper.text()).toContain('待确认已读') expect(wrapper.text()).toContain('查看通知') expect(wrapper.text()).toContain('查看邮件会话') + expect(wrapper.text()).toContain('该邮件不关联订单,请查看邮件内容。') + expect(wrapper.text()).not.toContain('S10/S99') expect(wrapper.text()).not.toContain('查看订单') }) - it('filters V4 workbench with supported backend fields only', async () => { + it('filters the reservation workbench with user-facing filter choices only', async () => { vi.mocked(service.fetchReservationV4WorkbenchItems) .mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 })) .mockResolvedValueOnce(createV4SourceNotificationWorkbenchResult('S10')) @@ -877,14 +884,18 @@ describe('reservation P0 views', () => { const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() - const itemTypeOptions = wrapper.find('select[name="item_type"]').findAll('option') + const itemTypeOptions = wrapper.find('select[name="business_status"]').findAll('option') .map((option) => option.attributes('value')) - expect(itemTypeOptions).toEqual(['', 'ORDER_TASK', 'SOURCE_NOTIFICATION']) - expect(wrapper.find('select[name="item_type"]').text()).toContain('来源通知') - expect(wrapper.find('select[name="item_type"]').text()).not.toContain('INFORMATIONAL_MESSAGE') + expect(itemTypeOptions).toEqual(['', 'PENDING_CONFIRM', 'REVIEW_REQUIRED', 'COMPLETED', 'SOURCE_NOTIFICATION']) + expect(wrapper.find('select[name="business_status"]').text()).toContain('来源通知') + expect(wrapper.find('select[name="business_status"]').text()).not.toContain('ORDER_TASK') + expect(wrapper.find('select[name="business_status"]').text()).not.toContain('SOURCE_NOTIFICATION') + expect(wrapper.find('select[name="item_type"]').exists()).toBe(false) + expect(wrapper.find('select[name="order_task_status"]').exists()).toBe(false) + expect(wrapper.find('select[name="card_status"]').exists()).toBe(false) - await wrapper.find('select[name="item_type"]').setValue('SOURCE_NOTIFICATION') + await wrapper.find('select[name="business_status"]').setValue('SOURCE_NOTIFICATION') await vi.dynamicImportSettled() expect(service.fetchReservationV4WorkbenchItems).toHaveBeenLastCalledWith( @@ -903,35 +914,45 @@ describe('reservation P0 views', () => { expect(lastTaskRequest).not.toHaveProperty('route_code') }) - it('filters and paginates V4 business order tasks with supported backend fields', async () => { + it('shows a filter-specific empty state for completed reservation items', async () => { + vi.mocked(service.fetchReservationV4WorkbenchItems) + .mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request')) + vi.mocked(service.fetchReservationV4OrderTasks) + .mockResolvedValueOnce({ + items: [], + page: { + page_num: 1, + page_size: 20, + total: 0, + }, + }) + + const wrapper = await mountWithPlugins(ReservationTaskListView) + await vi.dynamicImportSettled() + + await wrapper.find('select[name="business_status"]').setValue('COMPLETED') + await vi.dynamicImportSettled() + + expect(wrapper.text()).toContain('暂无已完成的预订事项') + expect(wrapper.text()).not.toContain('暂无需要处理的预订事项') + }) + + it('filters and paginates reservation work items with supported backend fields', async () => { vi.mocked(service.fetchReservationV4WorkbenchItems) .mockResolvedValueOnce(createV4WorkbenchResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 })) vi.mocked(service.fetchReservationV4OrderTasks) - .mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 })) - .mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 })) .mockResolvedValueOnce(createV4OrderTaskListResult('GRP-001', 'Booking Request', { page_num: 1, page_size: 20, total: 45 })) .mockResolvedValueOnce(createV4OrderTaskListResult('GRP-002', 'Booking Update', { page_num: 2, page_size: 20, total: 45 })) const wrapper = await mountWithPlugins(ReservationTaskListView) await vi.dynamicImportSettled() - expect(wrapper.find('select[name="order_task_status"]').attributes('disabled')).toBeDefined() - expect(wrapper.find('select[name="card_status"]').attributes('disabled')).toBeDefined() - - await wrapper.find('select[name="item_type"]').setValue('ORDER_TASK') - await vi.dynamicImportSettled() - expect(wrapper.find('select[name="order_task_status"]').attributes('disabled')).toBeUndefined() - expect(wrapper.find('select[name="card_status"]').attributes('disabled')).toBeUndefined() - - await wrapper.find('select[name="order_task_status"]').setValue('OPEN') - await vi.dynamicImportSettled() - await wrapper.find('select[name="card_status"]').setValue('REVIEW_REQUIRED') + await wrapper.find('select[name="business_status"]').setValue('REVIEW_REQUIRED') await vi.dynamicImportSettled() const taskListCalls = vi.mocked(service.fetchReservationV4OrderTasks).mock.calls const filteredTaskRequest = taskListCalls[taskListCalls.length - 1]?.[0] ?? {} expect(filteredTaskRequest).toMatchObject({ - order_task_status: 'OPEN', card_status: 'REVIEW_REQUIRED', page_num: 1, }) @@ -941,7 +962,6 @@ describe('reservation P0 views', () => { expect(service.fetchReservationV4OrderTasks).toHaveBeenLastCalledWith( expect.objectContaining({ - order_task_status: 'OPEN', card_status: 'REVIEW_REQUIRED', page_num: 2, page_size: 20, diff --git a/client/src/views/reservation/ReservationOrderDetailView.vue b/client/src/views/reservation/ReservationOrderDetailView.vue index 77d27d6..cae538f 100644 --- a/client/src/views/reservation/ReservationOrderDetailView.vue +++ b/client/src/views/reservation/ReservationOrderDetailView.vue @@ -128,27 +128,13 @@