优化V4订单事项办理页视觉交互
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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