实现V4 Trace事项卡前端展示

This commit is contained in:
andy
2026-07-21 18:15:19 +07:00
parent 38661257d1
commit f3b6adda17
10 changed files with 931 additions and 7 deletions

View File

@@ -104,7 +104,7 @@ describe('reservation V4 pages', () => {
it('keeps V4 task card layout classes for styled card metadata and payload summary', async () => {
const detail = createOrderTaskDetail()
detail.business_cards[0]!.card_type = 'TRACE_RESERVATION_NOTES'
detail.business_cards[0]!.card_type = 'VOUCHER'
detail.business_cards[0]!.display_payload = {
booking_scenario: 'STANDARD',
relevant_message_excerpt: 'Please keep this visible.',
@@ -514,6 +514,178 @@ describe('reservation V4 pages', () => {
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
})
it('renders GENERAL Trace cards with fixed department options and confirms safe trace fields only', async () => {
const detail = createOrderTaskDetail()
useTraceBusinessCard(detail, 'GENERAL')
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({
...detail,
business_cards: [
{
...detail.business_cards[0]!,
card_status: 'CONFIRMED',
},
],
})
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const traceSection = wrapper
.findAll('.task-card-section')
.find((section) => section.find('[data-testid="trace-card"]').exists())
expect(traceSection).toBeTruthy()
expect((traceSection!.find('textarea[name="/trace_items/0/text"]').element as HTMLTextAreaElement).value)
.toBe('Late arrival note')
expect(traceSection!.text()).not.toContain('CONTENT_SHOULD_NOT_RENDER')
expect(traceSection!.text()).not.toContain('TARGET_ORDER_SHOULD_NOT_RENDER')
expect(traceSection!.text()).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
expect(traceSection!.text()).not.toContain('https://oss.example/private/trace.pdf')
expect(traceSection!.find('details.safe-payload').exists()).toBe(false)
const departmentOptions = traceSection!
.find('select[name="/trace_items/0/department_code"]')
.findAll('option')
.map((option) => option.attributes('value'))
expect(departmentOptions).toEqual(['', 'FO', 'HSK', 'FO+HSK'])
await traceSection!.find('textarea[name="/trace_items/0/text"]').setValue('Arrange late arrival follow-up')
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('HSK')
await traceSection!.find('button.primary-button').trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-trace', {
version: 10,
confirmed_payload: {
trace_items: [
{
text: 'Arrange late arrival follow-up',
department_code: 'HSK',
},
],
},
})
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
expect(JSON.stringify(submittedRequest)).not.toContain('content')
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
expect(JSON.stringify(submittedRequest)).not.toContain('AI_PAYLOAD_SHOULD_NOT_RENDER')
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
})
it('renders EXTRA_BED Trace cards with room type lookup, positive count input and confirms safe fields only', async () => {
const detail = createOrderTaskDetail()
useTraceBusinessCard(detail, 'EXTRA_BED')
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const traceSection = wrapper
.findAll('.task-card-section')
.find((section) => section.find('[data-testid="trace-card"]').exists())
expect(traceSection).toBeTruthy()
expect(traceSection!.text()).toContain('SET EXTRA BED')
expect(traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').text()).toContain('TWN - Twin')
const roomCountInput = traceSection!.find('input[name="/trace_items/0/extra_bed_room_count"]')
expect(roomCountInput.attributes('type')).toBe('number')
expect(roomCountInput.attributes('min')).toBe('1')
expect(roomCountInput.attributes('step')).toBe('1')
await traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').setValue('TWN')
await roomCountInput.setValue('0')
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('FO+HSK')
await traceSection!.find('button.primary-button').trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
expect(traceSection!.text()).toContain('Extra Bed Room Count must be a positive integer')
await roomCountInput.setValue('2')
await traceSection!.find('button.primary-button').trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-trace', {
version: 10,
confirmed_payload: {
trace_items: [
{
target_room_type_code: 'TWN',
extra_bed_room_count: 2,
department_code: 'FO+HSK',
},
],
},
})
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
expect(JSON.stringify(submittedRequest)).not.toContain('content')
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
})
it('resolves REVIEW_REQUIRED Trace cards with field errors and safe review overrides', async () => {
const detail = createOrderTaskDetail()
useTraceBusinessCard(detail, 'GENERAL', 'REVIEW_REQUIRED')
detail.order_task.order_id = 'order-2001'
detail.order_task.target_resolution_status = 'RESOLVED'
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.resolveReservationV4OrderTaskCardReview).mockResolvedValue({
...detail,
business_cards: [
{
...detail.business_cards[0]!,
card_status: 'PENDING_CONFIRM',
review_status: 'RESOLVED',
},
],
})
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const traceSection = wrapper
.findAll('.task-card-section')
.find((section) => section.find('[data-testid="trace-card"]').exists())
expect(traceSection).toBeTruthy()
expect(traceSection!.find('.trace-field__error').text()).toContain('Department is required')
await traceSection!.find('textarea[name="/trace_items/0/text"]').setValue('Confirm trace review text')
await traceSection!.find('select[name="/trace_items/0/department_code"]').setValue('FO')
await traceSection!.find('textarea[name="v4_review_reason"]').setValue('confirmed trace note')
await traceSection!.find('button.primary-button').trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
expect(service.resolveReservationV4OrderTaskCardReview).toHaveBeenCalledWith('9001', 'card-trace', {
version: 10,
confirmed_order_id: 'order-2001',
reason: 'confirmed trace note',
field_overrides: [
{
field_pointer: '/trace_items/0/text',
value: 'Confirm trace review text',
},
{
field_pointer: '/trace_items/0/department_code',
value: 'FO',
},
],
})
const submittedRequest = vi.mocked(service.resolveReservationV4OrderTaskCardReview).mock.calls[0]?.[2]
expect(JSON.stringify(submittedRequest)).not.toContain('content')
expect(JSON.stringify(submittedRequest)).not.toContain('target_order')
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
})
it('renders New Booking Room Information as a business form and confirms stable final values', async () => {
const detail = createOrderTaskDetail({
businessEventType: 'NEW_BOOKING',
@@ -1557,6 +1729,124 @@ function useBoundOrder(detail: ReservationV4OrderTaskDetailResult): void {
}
}
function useTraceBusinessCard(
detail: ReservationV4OrderTaskDetailResult,
itemType: 'GENERAL' | 'EXTRA_BED',
cardStatus = 'PENDING_CONFIRM',
): void {
const isReviewRequired = cardStatus === 'REVIEW_REQUIRED'
detail.business_cards = [
createCard('card-trace', 'TRACE_RESERVATION_NOTES', cardStatus, {
event_type: 'TRACE_RESERVATION_NOTES',
version: 10,
availability: createAvailability({
confirmable: cardStatus === 'PENDING_CONFIRM',
reviewable: isReviewRequired,
editable: true,
read_only: false,
}),
display_payload: {
trace_items: [
itemType === 'EXTRA_BED'
? {
item_type: 'EXTRA_BED',
target_room_type_code: 'TWN',
extra_bed_room_count: 1,
department_code: 'FO',
content: 'CONTENT_SHOULD_NOT_RENDER',
}
: {
item_type: 'GENERAL',
text: 'Late arrival note',
department_code: 'FO',
content: 'CONTENT_SHOULD_NOT_RENDER',
},
],
target_order: 'TARGET_ORDER_SHOULD_NOT_RENDER',
ai_payload_json: 'AI_PAYLOAD_SHOULD_NOT_RENDER',
attachment_url: 'https://oss.example/private/trace.pdf',
},
fields: itemType === 'EXTRA_BED'
? [
createField('/trace_items/0/target_room_type_code', {
display_name: 'Target Room Type',
value: 'TWN',
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
control_type: 'SELECT',
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
}),
createField('/trace_items/0/extra_bed_room_count', {
display_name: 'Extra Bed Room Count',
value: 1,
control_type: 'NUMBER',
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
}),
createTraceDepartmentField(isReviewRequired),
createField('/trace_items/0/content', {
display_name: 'Legacy content',
value: 'CONTENT_SHOULD_NOT_RENDER',
editable: true,
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
}),
]
: [
createField('/trace_items/0/text', {
display_name: 'Trace Text',
value: 'Late arrival note',
control_type: 'TEXTAREA',
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
}),
createTraceDepartmentField(isReviewRequired, isReviewRequired ? ['Department is required'] : []),
createField('/trace_items/0/content', {
display_name: 'Legacy content',
value: 'CONTENT_SHOULD_NOT_RENDER',
editable: true,
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
}),
],
}),
]
detail.card_counts = {
...detail.card_counts,
pending_confirm_count: cardStatus === 'PENDING_CONFIRM' ? 2 : 1,
review_required_count: isReviewRequired ? 1 : 0,
}
}
function createTraceDepartmentField(
isReviewRequired: boolean,
validationErrors: string[] = [],
): ReservationV4TaskCardResult['fields'][number] {
return createField('/trace_items/0/department_code', {
display_name: 'Department',
value: 'FO',
control_type: 'SELECT',
options_source: 'reservation_v4_trace_department_fixed',
fixed_options: [
{
value: 'FO',
label: 'FO',
},
{
value: 'HSK',
label: 'HSK',
},
{
value: 'FO+HSK',
label: 'FO+HSK',
},
],
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
write_target: isReviewRequired ? 'REVIEW_RESOLUTION_FIELD_OVERRIDES' : 'CONFIRMED_PAYLOAD_JSON',
validation_errors: validationErrors,
})
}
function createPaymentConversationMedia(): SourceMessageOriginalMedia[] {
return [
{
@@ -1830,6 +2120,7 @@ function createField(
edit_scope: overrides.edit_scope ?? 'NORMAL_TASK',
write_target: overrides.write_target ?? 'CONFIRMED_PAYLOAD_JSON',
options_source: overrides.options_source ?? null,
fixed_options: overrides.fixed_options ?? null,
raw_readonly: overrides.raw_readonly ?? false,
validation_errors: overrides.validation_errors ?? [],
control_hint: overrides.control_hint ?? null,