优化V4订单事项办理页视觉交互
This commit is contained in:
1 parent
8c1221d2b0
commit
542c872597
8 files changed
+1077
-156
No files matched your search
+3
-3
@@ -4,14 +4,14 @@
|
||||
| --- | --- |
|
||||
| 最近更新 | 2026-07-22 |
|
||||
| 当前分支 | `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 卡后端字段契约收口、Trace 确认态字段刷新、Rooming List 事项确认卡文档口径、V4 复核态卡片交互和字段白名单文档口径,以及 V4 工作台 / 订单详情 / 任务详情普通酒店员工用户化展示与 polish 收口 |
|
||||
| 当前阶段 | 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 工作台 / 订单详情 / 任务详情普通酒店员工用户化展示与 polish 收口,以及订单事项办理页克制业务办理台视觉 polish |
|
||||
| 当前重点 | 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 原始响应等敏感字段;Basic Information 的 Market Code / Source Code 前端已改为可编辑字段,普通业务页不再展示字段下方 control hint、lookup 空目录提示或“只读”胶囊。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`,不按文件名猜测;`attachmLine truncated
|
||||
|
||||
## 1. 当前 Checkpoint
|
||||
|
||||
- 名称:`M002-V4-reservation-workflow-user-facing-polish-frontend`
|
||||
- 状态:Done,前端已完成测试机 smoke 暴露的 V4 用户化残留收口;默认页面继续保持普通酒店员工视角,不作为 V4 模型 / Task Card 调试视图展示。
|
||||
- 目标:`/reservation/tasks` 顶部导航使用“预订事项”;`/reservation/orders/{orderId}` 使用“订单总览”;`/reservation/order-tasks/{orderTaskId}` 使用“订单事项办理”。V4 Room Information 业务类型从 `NEW_BOOKING` / `UPDATE_BOOKING` / `CANCEL_BOOKING` 映射为“新预订 / 修改预订 / 取消预订”等用户文案;普通业务卡默认隐藏 lookup stale / warning 等目录技术提示;窄屏下每张事项卡的“确认卡片”按钮保持在卡片动作区右侧。
|
||||
- 状态:Done,前端已完成测试机 smoke 暴露的 V4 用户化残留收口,并完成订单事项办理页“克制的业务办理台”视觉 polish;默认页面继续保持普通酒店员工视角,不作为 V4 模型 / Task Card 调试视图展示。
|
||||
- 目标:`/reservation/tasks` 顶部导航使用“预订事项”;`/reservation/orders/{orderId}` 使用“订单总览”;`/reservation/order-tasks/{orderTaskId}` 使用“订单事项办理”。V4 Room Information 业务类型从 `NEW_BOOKING` / `UPDATE_BOOKING` / `CANCEL_BOOKING` 映射为“新预订 / 修改预订 / 取消预订”等用户文案;普通业务卡默认隐藏 lookup stale / warning 等目录技术提示;订单事项办理页首屏摘要突出订单线索、来源邮件、事项状态和卡片进度;每张事项卡的标题、提示、字段、消息和“确认卡片”按钮保持清晰层级,窄屏下主按钮仍在卡片动作区右侧。
|
||||
- 边界:本 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 自行拼装。
|
||||
|
||||
|
||||
@@ -26,44 +26,11 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ReservationV4RoomInformationCard
|
||||
v-if="isRoomInformationCard"
|
||||
class="task-card-section__fields"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<template v-else>
|
||||
<ReservationV4RoomingListCard
|
||||
v-if="isRoomingListCard"
|
||||
:order-reference="orderReference"
|
||||
:target-locator="targetLocator"
|
||||
:bound-order-id="boundOrderId"
|
||||
:bound-order-display-name="boundOrderDisplayName"
|
||||
/>
|
||||
<ReservationV4PaymentAttachmentPreview
|
||||
v-else-if="isPaymentCard"
|
||||
:card="card"
|
||||
:source-message-id="sourceMessageId"
|
||||
/>
|
||||
<ReservationV4TraceCard
|
||||
v-else-if="isTraceCard"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-else
|
||||
<div class="task-card-section__body">
|
||||
<ReservationV4RoomInformationCard
|
||||
v-if="isRoomInformationCard"
|
||||
class="task-card-section__fields"
|
||||
:fields="visibleFields"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
@@ -71,50 +38,85 @@
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="actionErrors.length"
|
||||
class="action-message action-message--error"
|
||||
>
|
||||
<p
|
||||
v-for="message in actionErrors"
|
||||
:key="message"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="successMessage"
|
||||
class="action-message action-message--success"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="card.card_status === 'REVIEW_REQUIRED'"
|
||||
class="review-box"
|
||||
>
|
||||
<label>
|
||||
<span>{{ t('taskV4.confirmedOrderId') }}</span>
|
||||
<input
|
||||
name="v4_confirmed_order_id"
|
||||
:value="reviewForm.confirmed_order_id"
|
||||
:placeholder="t('taskV4.confirmedOrderIdPlaceholder')"
|
||||
@input="updateReviewForm('confirmed_order_id', $event)"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('taskV4.reviewReason') }}</span>
|
||||
<textarea
|
||||
name="v4_review_reason"
|
||||
:value="reviewForm.reason"
|
||||
:placeholder="t('taskV4.reviewReasonPlaceholder')"
|
||||
rows="3"
|
||||
@input="updateReviewForm('reason', $event)"
|
||||
<template v-else>
|
||||
<ReservationV4RoomingListCard
|
||||
v-if="isRoomingListCard"
|
||||
:order-reference="orderReference"
|
||||
:target-locator="targetLocator"
|
||||
:bound-order-id="boundOrderId"
|
||||
:bound-order-display-name="boundOrderDisplayName"
|
||||
/>
|
||||
</label>
|
||||
<ReservationV4PaymentAttachmentPreview
|
||||
v-else-if="isPaymentCard"
|
||||
:card="card"
|
||||
:source-message-id="sourceMessageId"
|
||||
/>
|
||||
<ReservationV4TraceCard
|
||||
v-else-if="isTraceCard"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-else
|
||||
class="task-card-section__fields"
|
||||
:fields="visibleFields"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="actionErrors.length"
|
||||
class="action-message action-message--error"
|
||||
>
|
||||
<p
|
||||
v-for="message in actionErrors"
|
||||
:key="message"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="successMessage"
|
||||
class="action-message action-message--success"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="card.card_status === 'REVIEW_REQUIRED'"
|
||||
class="review-box"
|
||||
>
|
||||
<label>
|
||||
<span>{{ t('taskV4.confirmedOrderId') }}</span>
|
||||
<input
|
||||
name="v4_confirmed_order_id"
|
||||
:value="reviewForm.confirmed_order_id"
|
||||
:placeholder="t('taskV4.confirmedOrderIdPlaceholder')"
|
||||
@input="updateReviewForm('confirmed_order_id', $event)"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('taskV4.reviewReason') }}</span>
|
||||
<textarea
|
||||
name="v4_review_reason"
|
||||
:value="reviewForm.reason"
|
||||
:placeholder="t('taskV4.reviewReasonPlaceholder')"
|
||||
rows="3"
|
||||
@input="updateReviewForm('reason', $event)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -261,15 +263,18 @@ function handlePrimaryAction(): void {
|
||||
<style scoped>
|
||||
.task-card-section {
|
||||
overflow: hidden;
|
||||
border-color: var(--th-color-slate-200);
|
||||
box-shadow: var(--th-shadow-sm);
|
||||
}
|
||||
|
||||
.task-card-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.task-card-section__heading {
|
||||
@@ -289,21 +294,27 @@ function handlePrimaryAction(): void {
|
||||
.card-notice-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid var(--th-color-slate-100);
|
||||
background: var(--th-color-slate-50);
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
background: var(--th-color-info-bg);
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.card-notice-list p {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-600);
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-card-section__body {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px 20px 16px;
|
||||
}
|
||||
|
||||
.task-card-section__fields {
|
||||
padding: 0 20px 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@@ -337,7 +348,9 @@ function handlePrimaryAction(): void {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 0 20px 20px;
|
||||
border-top: 1px solid var(--th-color-slate-100);
|
||||
background: var(--th-color-white);
|
||||
padding: 14px 20px 18px;
|
||||
}
|
||||
|
||||
.task-card-section__footer-actions {
|
||||
@@ -351,9 +364,10 @@ function handlePrimaryAction(): void {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
border-top: 1px solid var(--th-color-slate-200);
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 16px 20px 18px;
|
||||
border: 1px solid var(--th-color-warning-bg);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-warning-bg);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.review-box label {
|
||||
@@ -362,9 +376,9 @@ function handlePrimaryAction(): void {
|
||||
}
|
||||
|
||||
.review-box span {
|
||||
color: var(--th-color-slate-500);
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.review-box input,
|
||||
@@ -386,10 +400,10 @@ function handlePrimaryAction(): void {
|
||||
}
|
||||
|
||||
.action-message {
|
||||
margin: 0 20px 18px;
|
||||
margin: 0;
|
||||
border-radius: var(--th-radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
@@ -414,18 +428,23 @@ function handlePrimaryAction(): void {
|
||||
.card-order-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--th-color-blue-600);
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card-order-link:hover {
|
||||
text-decoration: underline;
|
||||
border-color: var(--th-color-blue-600);
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.task-card-section__header,
|
||||
.review-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -572,6 +572,16 @@ export default {
|
||||
title: 'Reservation item handling',
|
||||
subtitle: 'Handle the current reservation item from the source email.',
|
||||
orderTaskSummary: 'Item summary',
|
||||
handlingDesk: 'Handling overview',
|
||||
handlingDeskHint: 'Check the order clue and source, then confirm each item.',
|
||||
sourceEmail: 'Source email',
|
||||
sourceSender: 'Sender',
|
||||
sourceReceivedAt: 'Received at',
|
||||
cardProgress: 'Card progress',
|
||||
totalCards: 'Total',
|
||||
pendingCards: 'Pending',
|
||||
reviewCards: 'Review',
|
||||
confirmedCards: 'Confirmed',
|
||||
orderTaskId: 'Item ID',
|
||||
orderReference: 'Order reference',
|
||||
targetBookingType: 'Booking type',
|
||||
|
||||
@@ -572,6 +572,16 @@ export default {
|
||||
title: 'จัดการรายการจอง',
|
||||
subtitle: 'จัดการรายการจองปัจจุบันจากอีเมลต้นทาง',
|
||||
orderTaskSummary: 'สรุปรายการ',
|
||||
handlingDesk: 'ภาพรวมการจัดการ',
|
||||
handlingDeskHint: 'ตรวจข้อมูลออเดอร์และอีเมลต้นทาง แล้วค่อยยืนยันแต่ละรายการ',
|
||||
sourceEmail: 'อีเมลต้นทาง',
|
||||
sourceSender: 'ผู้ส่ง',
|
||||
sourceReceivedAt: 'เวลาที่ได้รับ',
|
||||
cardProgress: 'ความคืบหน้าการ์ด',
|
||||
totalCards: 'ทั้งหมด',
|
||||
pendingCards: 'รอยืนยัน',
|
||||
reviewCards: 'รอตรวจสอบ',
|
||||
confirmedCards: 'ยืนยันแล้ว',
|
||||
orderTaskId: 'รหัสรายการ',
|
||||
orderReference: 'อ้างอิงออเดอร์',
|
||||
targetBookingType: 'ประเภทการจอง',
|
||||
|
||||
@@ -572,6 +572,16 @@ export default {
|
||||
title: '订单事项办理',
|
||||
subtitle: '按来源邮件办理当前订单事项',
|
||||
orderTaskSummary: '事项摘要',
|
||||
handlingDesk: '办理概览',
|
||||
handlingDeskHint: '先核对订单线索和来源,再逐张确认事项。',
|
||||
sourceEmail: '来源邮件',
|
||||
sourceSender: '发件人',
|
||||
sourceReceivedAt: '收到时间',
|
||||
cardProgress: '卡片进度',
|
||||
totalCards: '全部',
|
||||
pendingCards: '待确认',
|
||||
reviewCards: '待复核',
|
||||
confirmedCards: '已确认',
|
||||
orderTaskId: '事项编号',
|
||||
orderReference: '订单引用',
|
||||
targetBookingType: '预订类型',
|
||||
|
||||
@@ -145,6 +145,30 @@ describe('reservation V4 pages', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('presents the order task as a restrained handling desk summary', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const summary = wrapper.find('.handling-summary')
|
||||
expect(summary.exists()).toBe(true)
|
||||
expect(summary.text()).toContain('GRP-001')
|
||||
expect(summary.text()).toContain('Booking Request')
|
||||
expect(summary.text()).toContain('guest@example.test')
|
||||
expect(summary.text()).toContain(zhCN.taskV4.cardProgress)
|
||||
expect(summary.text()).toContain('3')
|
||||
expect(summary.text()).toContain('2')
|
||||
expect(summary.text()).toContain(zhCN.taskV4.pendingCards)
|
||||
expect(summary.text()).toContain(zhCN.taskV4.confirmedCards)
|
||||
expect(summary.text()).not.toContain('9001')
|
||||
expect(summary.text()).not.toContain(zhCN.taskV4.orderTaskId)
|
||||
})
|
||||
|
||||
it('keeps task card layout classes and places the primary card action in the card footer', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
detail.business_cards[0]!.card_type = 'VOUCHER'
|
||||
@@ -162,12 +186,15 @@ 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('.task-card-section__body').exists()).toBe(true)
|
||||
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').exists()).toBe(false)
|
||||
expect(wrapper.find('.task-card-section__footer .card-order-link').exists()).toBe(false)
|
||||
const firstCard = wrapper.find('.task-card-section')
|
||||
const footer = firstCard.find('.task-card-section__footer')
|
||||
expect(footer.exists()).toBe(true)
|
||||
expect(firstCard.find('.task-card-section__body').exists()).toBe(true)
|
||||
const cardChildren = Array.from(firstCard.element.children)
|
||||
expect(cardChildren[cardChildren.length - 1]).toBe(footer.element)
|
||||
expect(footer.find('.task-card-section__footer-actions .primary-button').text()).toContain(zhCN.taskV4.confirmCard)
|
||||
@@ -1375,7 +1402,8 @@ describe('reservation V4 pages', () => {
|
||||
it('keeps the per-card primary action right aligned on narrow screens', () => {
|
||||
expect(taskCardSectionSource).toContain('@media (max-width: 980px)')
|
||||
expect(taskCardSectionSource).toContain('.task-card-section__footer-actions')
|
||||
expect(taskCardSectionSource).toContain('padding: 0 20px 20px')
|
||||
expect(taskCardSectionSource).toContain('border-top: 1px solid var(--th-color-slate-100)')
|
||||
expect(taskCardSectionSource).toContain('padding: 14px 20px 18px')
|
||||
expect(taskCardSectionSource).toContain('justify-content: flex-end')
|
||||
expect(taskCardSectionSource).toContain('.task-card-section__footer {\n align-items: flex-end;\n }')
|
||||
})
|
||||
|
||||
@@ -28,52 +28,63 @@
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<template v-else-if="detail">
|
||||
<section class="th-section summary-section">
|
||||
<div class="th-section-header">
|
||||
<section class="th-section handling-summary">
|
||||
<div class="handling-summary__hero">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('taskV4.orderTaskSummary') }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ detail.source_message_summary?.subject ?? t('taskList.noSourceSubject') }}
|
||||
</p>
|
||||
<span class="handling-summary__eyebrow">{{ t('taskV4.handlingDesk') }}</span>
|
||||
<h2>{{ displayOrderReference }}</h2>
|
||||
<p>{{ detail.source_message_summary?.subject ?? t('taskList.noSourceSubject') }}</p>
|
||||
</div>
|
||||
<ReservationStatusBadge :status="detail.order_task.order_task_status" />
|
||||
</div>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div>
|
||||
<span>{{ t('taskV4.orderReference') }}</span>
|
||||
<strong>{{ displayOrderReference }}</strong>
|
||||
<div class="handling-summary__content">
|
||||
<div class="handling-summary__facts">
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetBookingType') }}</span>
|
||||
<strong>{{ bookingTypeLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetLocator') }}</span>
|
||||
<strong>{{ targetLocator }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetResolutionStatus') }}</span>
|
||||
<strong>{{ targetResolutionLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.sourceSender') }}</span>
|
||||
<strong>{{ detail.source_message_summary?.sender_summary ?? '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.sourceReceivedAt') }}</span>
|
||||
<strong>{{ formatReservationDateTime(detail.order_task.source_received_at) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetBookingType') }}</span>
|
||||
<strong>{{ bookingTypeLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetLocator') }}</span>
|
||||
<strong>{{ targetLocator }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetResolutionStatus') }}</span>
|
||||
<strong>{{ targetResolutionLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.cardCounts') }}</span>
|
||||
<strong>
|
||||
{{ t('taskList.cardCountsTotal', { total: detail.card_counts.total_count }) }}
|
||||
</strong>
|
||||
<small>
|
||||
{{ t('taskList.cardCountsPending', { count: detail.card_counts.pending_confirm_count }) }}
|
||||
·
|
||||
{{ t('taskList.cardCountsReview', { count: detail.card_counts.review_required_count }) }}
|
||||
·
|
||||
{{ t('taskList.cardCountsConfirmed', { count: detail.card_counts.confirmed_count }) }}
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('task.sourceReceivedAt') }}</span>
|
||||
<strong>{{ formatReservationDateTime(detail.order_task.source_received_at) }}</strong>
|
||||
|
||||
<div
|
||||
class="handling-progress"
|
||||
:aria-label="t('taskV4.cardProgress')"
|
||||
>
|
||||
<span class="handling-progress__label">{{ t('taskV4.cardProgress') }}</span>
|
||||
<div class="handling-progress__items">
|
||||
<div class="handling-progress__item handling-progress__item--total">
|
||||
<span>{{ t('taskV4.totalCards') }}</span>
|
||||
<strong>{{ detail.card_counts.total_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--pending">
|
||||
<span>{{ t('taskV4.pendingCards') }}</span>
|
||||
<strong>{{ detail.card_counts.pending_confirm_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--review">
|
||||
<span>{{ t('taskV4.reviewCards') }}</span>
|
||||
<strong>{{ detail.card_counts.review_required_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--confirmed">
|
||||
<span>{{ t('taskV4.confirmedCards') }}</span>
|
||||
<strong>{{ detail.card_counts.confirmed_count }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -662,39 +673,127 @@ function formatApiError(error: unknown, fallback: string): {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.summary-section,
|
||||
.handling-summary,
|
||||
.diagnostics-section,
|
||||
.card-stack {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0 20px 20px;
|
||||
.handling-summary {
|
||||
border-color: color-mix(in srgb, var(--th-color-slate-200) 80%, var(--th-color-blue-600));
|
||||
box-shadow: var(--th-shadow-panel);
|
||||
}
|
||||
|
||||
.summary-grid div {
|
||||
.handling-summary__hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 22px 24px 18px;
|
||||
}
|
||||
|
||||
.handling-summary__eyebrow {
|
||||
color: var(--th-color-info);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.handling-summary__hero h2,
|
||||
.handling-summary__hero p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.handling-summary__hero h2 {
|
||||
margin-top: 6px;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.handling-summary__hero p {
|
||||
margin-top: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.handling-summary__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
||||
gap: 20px;
|
||||
padding: 18px 24px 22px;
|
||||
}
|
||||
|
||||
.handling-summary__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.handling-summary__facts div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-grid span {
|
||||
.handling-summary__facts span,
|
||||
.handling-progress__label,
|
||||
.handling-progress__item span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.summary-grid strong {
|
||||
.handling-summary__facts strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.summary-grid small {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
.handling-progress {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.handling-progress__items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.handling-progress__item {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.handling-progress__item strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.handling-progress__item--pending strong {
|
||||
color: var(--th-color-info);
|
||||
}
|
||||
|
||||
.handling-progress__item--review strong {
|
||||
color: var(--th-color-warning);
|
||||
}
|
||||
|
||||
.handling-progress__item--confirmed strong {
|
||||
color: var(--th-color-success);
|
||||
}
|
||||
|
||||
.card-stack {
|
||||
@@ -757,7 +856,19 @@ function formatApiError(error: unknown, fallback: string): {
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.summary-grid {
|
||||
.page-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handling-summary__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.handling-summary__hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handling-summary__facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
# V4 Order Task Detail Visual Polish Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
||||
|
||||
**Goal:** Polish `/reservation/order-tasks/{orderTaskId}` into a restrained business handling page with clearer summary, card hierarchy, progress, and action placement.
|
||||
|
||||
**Architecture:** Keep the existing V4 order task detail data flow and card components. Add presentation-only computed values and CSS classes in the page component, refine the single-card shell component, and extend i18n copy without changing service calls or payload builders.
|
||||
|
||||
**Tech Stack:** Vue 3.5, TypeScript 6, Vite 8, PrimeVue 4, vue-i18n 11, Vitest 4, Vue Test Utils 2.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Do not change `/api/reservation/order-tasks/**` request or response contracts.
|
||||
- Do not change card confirm, review resolution, field whitelist, catalog validation, permissions, audit, or hotel isolation behavior.
|
||||
- Do not expose V4 model terms, Task Card, JSON Pointer, payload, version, internal IDs, raw email body, raw attachment URLs, or AI raw payload in the default main page.
|
||||
- Keep Basic Information first, business cards second, and SourceMessage display at the bottom.
|
||||
- Use existing `th-*` CSS tokens and project CSS patterns; do not add Tailwind, shadcn, or an Admin Template.
|
||||
- Add or update user-facing copy through i18n locale files, not hardcoded template strings.
|
||||
- Existing `.idea/*` local changes are unrelated and must not be staged or reverted.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `client/src/tests/reservationV4Views.spec.ts`: add coverage for the new handling summary, progress chips, and refined card shell classes while preserving safety assertions.
|
||||
- Modify `client/src/views/reservation/ReservationV4OrderTaskDetailView.vue`: reshape the top summary, add card progress display, and update page-level scoped CSS.
|
||||
- Modify `client/src/components/reservation/ReservationV4TaskCardSection.vue`: refine the card shell, notice area, review box, action message, and footer layout without changing emitted events.
|
||||
- Modify `client/src/i18n/locales/zh-CN.ts`: add Chinese labels for handling summary and card progress.
|
||||
- Modify `client/src/i18n/locales/en-US.ts`: add English labels for handling summary and card progress.
|
||||
- Modify `client/src/i18n/locales/th-TH.ts`: add Thai labels for handling summary and card progress.
|
||||
- Modify `PROJECT_STATE.md`: record that the V4 order task detail visual polish checkpoint has been completed if implementation and verification pass.
|
||||
|
||||
## Task 1: Lock The Visual Contract With Focused Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/tests/reservationV4Views.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: existing `createOrderTaskDetail()` fixture and mounted `ReservationV4OrderTaskDetailView`.
|
||||
- Produces: test expectations for `.handling-summary`, `.handling-progress`, `.task-card-section__body`, and card footer placement.
|
||||
|
||||
- [x] **Step 1: Add a failing summary test**
|
||||
|
||||
Add this test inside `describe('reservation V4 pages', () => { ... })` near the existing order task detail tests:
|
||||
|
||||
```ts
|
||||
it('presents the order task as a restrained handling desk summary', async () => {
|
||||
const detail = createOrderTaskDetail()
|
||||
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
|
||||
|
||||
const wrapper = await mountWithPlugins(
|
||||
ReservationV4OrderTaskDetailView,
|
||||
'/reservation/order-tasks/9001',
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const summary = wrapper.find('.handling-summary')
|
||||
expect(summary.exists()).toBe(true)
|
||||
expect(summary.text()).toContain('GRP-001')
|
||||
expect(summary.text()).toContain('Booking Request')
|
||||
expect(summary.text()).toContain('guest@example.test')
|
||||
expect(summary.text()).toContain(zhCN.taskV4.cardProgress)
|
||||
expect(summary.text()).toContain('3')
|
||||
expect(summary.text()).toContain('2')
|
||||
expect(summary.text()).toContain(zhCN.taskV4.pendingCards)
|
||||
expect(summary.text()).toContain(zhCN.taskV4.confirmedCards)
|
||||
expect(summary.text()).not.toContain('9001')
|
||||
expect(summary.text()).not.toContain(zhCN.taskV4.orderTaskId)
|
||||
})
|
||||
```
|
||||
|
||||
- [x] **Step 2: Run the focused test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts -t "restrained handling desk summary"
|
||||
```
|
||||
|
||||
Expected: FAIL because `.handling-summary` and the new i18n keys do not exist yet.
|
||||
|
||||
- [x] **Step 3: Strengthen the existing card shell test**
|
||||
|
||||
In `keeps task card layout classes and places the primary card action in the card footer`, add these assertions after `expect(wrapper.find('.task-card-section__header').exists()).toBe(true)`:
|
||||
|
||||
```ts
|
||||
expect(wrapper.find('.task-card-section__body').exists()).toBe(true)
|
||||
expect(wrapper.find('.task-card-section__status-actions').exists()).toBe(true)
|
||||
expect(wrapper.find('.task-card-section__footer .card-order-link').exists()).toBe(false)
|
||||
```
|
||||
|
||||
Then add this assertion after `const footer = firstCard.find('.task-card-section__footer')`:
|
||||
|
||||
```ts
|
||||
expect(firstCard.find('.task-card-section__body').exists()).toBe(true)
|
||||
```
|
||||
|
||||
- [x] **Step 4: Run the focused card shell test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts -t "keeps task card layout classes"
|
||||
```
|
||||
|
||||
Expected: FAIL because `.task-card-section__body` does not exist yet.
|
||||
|
||||
## Task 2: Implement The Handling Summary
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/views/reservation/ReservationV4OrderTaskDetailView.vue`
|
||||
- Modify: `client/src/i18n/locales/zh-CN.ts`
|
||||
- Modify: `client/src/i18n/locales/en-US.ts`
|
||||
- Modify: `client/src/i18n/locales/th-TH.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `detail.order_task`, `detail.source_message_summary`, `detail.card_counts`, existing `bookingTypeLabel`, `targetLocator`, `targetResolutionLabel`, and `formatReservationDateTime`.
|
||||
- Produces: `.handling-summary`, `.handling-summary__hero`, `.handling-progress`, `.handling-progress__item`, and i18n keys under `taskV4`.
|
||||
|
||||
- [x] **Step 1: Add i18n keys**
|
||||
|
||||
Add these keys inside each locale's `taskV4` object near `orderTaskSummary`.
|
||||
|
||||
Chinese:
|
||||
|
||||
```ts
|
||||
handlingDesk: '办理概览',
|
||||
handlingDeskHint: '先核对订单线索和来源,再逐张确认事项。',
|
||||
sourceEmail: '来源邮件',
|
||||
sourceSender: '发件人',
|
||||
sourceReceivedAt: '收到时间',
|
||||
cardProgress: '卡片进度',
|
||||
totalCards: '全部',
|
||||
pendingCards: '待确认',
|
||||
reviewCards: '待复核',
|
||||
confirmedCards: '已确认',
|
||||
```
|
||||
|
||||
English:
|
||||
|
||||
```ts
|
||||
handlingDesk: 'Handling overview',
|
||||
handlingDeskHint: 'Check the order clue and source, then confirm each item.',
|
||||
sourceEmail: 'Source email',
|
||||
sourceSender: 'Sender',
|
||||
sourceReceivedAt: 'Received at',
|
||||
cardProgress: 'Card progress',
|
||||
totalCards: 'Total',
|
||||
pendingCards: 'Pending',
|
||||
reviewCards: 'Review',
|
||||
confirmedCards: 'Confirmed',
|
||||
```
|
||||
|
||||
Thai:
|
||||
|
||||
```ts
|
||||
handlingDesk: 'ภาพรวมการจัดการ',
|
||||
handlingDeskHint: 'ตรวจข้อมูลออเดอร์และอีเมลต้นทาง แล้วค่อยยืนยันแต่ละรายการ',
|
||||
sourceEmail: 'อีเมลต้นทาง',
|
||||
sourceSender: 'ผู้ส่ง',
|
||||
sourceReceivedAt: 'เวลาที่ได้รับ',
|
||||
cardProgress: 'ความคืบหน้าการ์ด',
|
||||
totalCards: 'ทั้งหมด',
|
||||
pendingCards: 'รอยืนยัน',
|
||||
reviewCards: 'รอตรวจสอบ',
|
||||
confirmedCards: 'ยืนยันแล้ว',
|
||||
```
|
||||
|
||||
- [x] **Step 2: Replace the existing summary section template**
|
||||
|
||||
In `ReservationV4OrderTaskDetailView.vue`, replace the current `<section class="th-section summary-section">...</section>` with:
|
||||
|
||||
```vue
|
||||
<section class="th-section handling-summary">
|
||||
<div class="handling-summary__hero">
|
||||
<div>
|
||||
<span class="handling-summary__eyebrow">{{ t('taskV4.handlingDesk') }}</span>
|
||||
<h2>{{ displayOrderReference }}</h2>
|
||||
<p>{{ detail.source_message_summary?.subject ?? t('taskList.noSourceSubject') }}</p>
|
||||
</div>
|
||||
<ReservationStatusBadge :status="detail.order_task.order_task_status" />
|
||||
</div>
|
||||
|
||||
<div class="handling-summary__content">
|
||||
<div class="handling-summary__facts">
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetBookingType') }}</span>
|
||||
<strong>{{ bookingTypeLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetLocator') }}</span>
|
||||
<strong>{{ targetLocator }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.targetResolutionStatus') }}</span>
|
||||
<strong>{{ targetResolutionLabel }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.sourceSender') }}</span>
|
||||
<strong>{{ detail.source_message_summary?.sender_summary ?? '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('taskV4.sourceReceivedAt') }}</span>
|
||||
<strong>{{ formatReservationDateTime(detail.order_task.source_received_at) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="handling-progress"
|
||||
:aria-label="t('taskV4.cardProgress')"
|
||||
>
|
||||
<span class="handling-progress__label">{{ t('taskV4.cardProgress') }}</span>
|
||||
<div class="handling-progress__items">
|
||||
<div class="handling-progress__item handling-progress__item--total">
|
||||
<span>{{ t('taskV4.totalCards') }}</span>
|
||||
<strong>{{ detail.card_counts.total_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--pending">
|
||||
<span>{{ t('taskV4.pendingCards') }}</span>
|
||||
<strong>{{ detail.card_counts.pending_confirm_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--review">
|
||||
<span>{{ t('taskV4.reviewCards') }}</span>
|
||||
<strong>{{ detail.card_counts.review_required_count }}</strong>
|
||||
</div>
|
||||
<div class="handling-progress__item handling-progress__item--confirmed">
|
||||
<span>{{ t('taskV4.confirmedCards') }}</span>
|
||||
<strong>{{ detail.card_counts.confirmed_count }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [x] **Step 3: Replace page summary CSS**
|
||||
|
||||
In the scoped style of `ReservationV4OrderTaskDetailView.vue`, replace `.summary-section`, `.summary-grid`, and related summary rules with the new `.handling-summary` rules:
|
||||
|
||||
```css
|
||||
.handling-summary,
|
||||
.diagnostics-section,
|
||||
.card-stack {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.handling-summary {
|
||||
border-color: color-mix(in srgb, var(--th-color-slate-200) 80%, var(--th-color-blue-600));
|
||||
box-shadow: var(--th-shadow-panel);
|
||||
}
|
||||
|
||||
.handling-summary__hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 22px 24px 18px;
|
||||
}
|
||||
|
||||
.handling-summary__eyebrow {
|
||||
color: var(--th-color-info);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.handling-summary__hero h2,
|
||||
.handling-summary__hero p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.handling-summary__hero h2 {
|
||||
margin-top: 6px;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.handling-summary__hero p {
|
||||
margin-top: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.handling-summary__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
||||
gap: 20px;
|
||||
padding: 18px 24px 22px;
|
||||
}
|
||||
|
||||
.handling-summary__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.handling-summary__facts div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.handling-summary__facts span,
|
||||
.handling-progress__label,
|
||||
.handling-progress__item span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.handling-summary__facts strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.handling-progress {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.handling-progress__items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.handling-progress__item {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.handling-progress__item strong {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.handling-progress__item--pending strong {
|
||||
color: var(--th-color-info);
|
||||
}
|
||||
|
||||
.handling-progress__item--review strong {
|
||||
color: var(--th-color-warning);
|
||||
}
|
||||
|
||||
.handling-progress__item--confirmed strong {
|
||||
color: var(--th-color-success);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 4: Update responsive CSS**
|
||||
|
||||
Replace the current summary media query with:
|
||||
|
||||
```css
|
||||
@media (max-width: 980px) {
|
||||
.page-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handling-summary__hero,
|
||||
.handling-summary__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.handling-summary__hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handling-summary__facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: Run Task 1 summary test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts -t "restrained handling desk summary"
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 3: Refine The Card Shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/components/reservation/ReservationV4TaskCardSection.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: existing props and emits from `ReservationV4TaskCardSection.vue`.
|
||||
- Produces: `.task-card-section__body` wrapper and refined CSS while keeping `confirm` and `resolveReview` emits unchanged.
|
||||
|
||||
- [x] **Step 1: Add a body wrapper around card content**
|
||||
|
||||
In `ReservationV4TaskCardSection.vue`, wrap the card renderer block, action messages, and review box in a new body container. Keep the footer outside this body so the existing footer-order test remains valid.
|
||||
|
||||
```vue
|
||||
<div class="task-card-section__body">
|
||||
<ReservationV4RoomInformationCard
|
||||
v-if="isRoomInformationCard"
|
||||
class="task-card-section__fields"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<template v-else>
|
||||
<ReservationV4RoomingListCard
|
||||
v-if="isRoomingListCard"
|
||||
:order-reference="orderReference"
|
||||
:target-locator="targetLocator"
|
||||
:bound-order-id="boundOrderId"
|
||||
:bound-order-display-name="boundOrderDisplayName"
|
||||
/>
|
||||
<ReservationV4PaymentAttachmentPreview
|
||||
v-else-if="isPaymentCard"
|
||||
:card="card"
|
||||
:source-message-id="sourceMessageId"
|
||||
/>
|
||||
<ReservationV4TraceCard
|
||||
v-else-if="isTraceCard"
|
||||
:card="card"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<ReservationV4TaskCardFieldRenderer
|
||||
v-else
|
||||
class="task-card-section__fields"
|
||||
:fields="visibleFields"
|
||||
:model-value="modelValue"
|
||||
:read-only="readOnly"
|
||||
:editable-keys="editableKeys"
|
||||
:validation-errors="validationErrors"
|
||||
:hotel-id="hotelId"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="actionErrors.length"
|
||||
class="action-message action-message--error"
|
||||
>
|
||||
<p
|
||||
v-for="message in actionErrors"
|
||||
:key="message"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="successMessage"
|
||||
class="action-message action-message--success"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="card.card_status === 'REVIEW_REQUIRED'"
|
||||
class="review-box"
|
||||
>
|
||||
<!-- keep the existing review labels, input, and textarea exactly as-is -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
When applying this step, move the existing review label/input/textarea markup into the wrapper unchanged.
|
||||
|
||||
- [x] **Step 2: Replace card shell CSS**
|
||||
|
||||
Replace the scoped CSS for `.task-card-section`, `.task-card-section__header`, `.card-notice-list`, `.task-card-section__fields`, `.review-box`, `.action-message`, `.card-order-link`, and the media query with:
|
||||
|
||||
```css
|
||||
.task-card-section {
|
||||
overflow: hidden;
|
||||
border-color: var(--th-color-slate-200);
|
||||
box-shadow: var(--th-shadow-sm);
|
||||
}
|
||||
|
||||
.task-card-section__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
background: linear-gradient(180deg, var(--th-color-white), var(--th-color-slate-50));
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.task-card-section__heading {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card-section__status-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-notice-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
background: var(--th-color-info-bg);
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.card-notice-list p {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-card-section__body {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px 20px 16px;
|
||||
}
|
||||
|
||||
.task-card-section__fields {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.review-box {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
border: 1px solid var(--th-color-warning-bg);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-warning-bg);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.review-box label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.review-box span {
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.review-box input,
|
||||
.review-box textarea {
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
font: inherit;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.review-box input:focus,
|
||||
.review-box textarea:focus {
|
||||
border-color: var(--th-color-blue-600);
|
||||
box-shadow: 0 0 0 3px var(--th-color-info-bg);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.action-message {
|
||||
margin: 0;
|
||||
border-radius: var(--th-radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.action-message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-message p + p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.action-message--error {
|
||||
background: var(--th-color-danger-bg);
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.action-message--success {
|
||||
background: var(--th-color-success-bg);
|
||||
color: var(--th-color-success);
|
||||
}
|
||||
|
||||
.card-order-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card-order-link:hover {
|
||||
border-color: var(--th-color-blue-600);
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.task-card-section__header,
|
||||
.review-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-card-section__header,
|
||||
.task-card-section__footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-card-section__status-actions,
|
||||
.task-card-section__footer-actions {
|
||||
align-self: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-card-section__footer {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep the existing `.primary-button`, `.task-card-section__footer`, and `.task-card-section__footer-actions` rules unless a later lint or visual issue requires small spacing adjustments.
|
||||
|
||||
- [x] **Step 3: Run the focused card shell test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts -t "keeps task card layout classes"
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 4: Verification And Project State
|
||||
|
||||
**Files:**
|
||||
- Modify: `PROJECT_STATE.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: completed frontend implementation and test results.
|
||||
- Produces: updated project state note for the completed visual polish checkpoint.
|
||||
|
||||
- [x] **Step 1: Run focused V4 view tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [x] **Step 2: Run lint and typecheck**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client lint
|
||||
CI=true pnpm --dir client typecheck
|
||||
```
|
||||
|
||||
Expected: PASS for both commands.
|
||||
|
||||
- [x] **Step 3: Update `PROJECT_STATE.md`**
|
||||
|
||||
In the top status table and current checkpoint section, record that the V4 order task detail page visual polish is complete. Use concise Chinese wording and explicitly note that no backend API or security boundary changed.
|
||||
|
||||
Expected wording to incorporate:
|
||||
|
||||
```md
|
||||
订单事项办理页已按“克制的业务办理台”方向完成前端视觉与交互 polish:首屏摘要突出订单线索、来源邮件、状态和卡片进度,卡片标题、提示、字段、消息和确认按钮层级更清楚;本次不改后端接口、确认 / 复核提交契约、权限、审计或安全边界。
|
||||
```
|
||||
|
||||
- [x] **Step 4: Run final focused tests after docs update**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
CI=true pnpm --dir client test -- reservationV4Views.spec.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [x] **Step 5: Review working tree**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: modified files are limited to the planned frontend files, locale files, `PROJECT_STATE.md`, and this plan/spec history. `.idea/*` may remain modified as unrelated local changes and must not be staged.
|
||||
|
||||
Reference in new issue
Block a user