修复任务字段过滤与控件可写规则
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
||||
buildInitialFieldValues,
|
||||
buildEditableFieldValues,
|
||||
groupReservationFields,
|
||||
isReservationFieldEditable,
|
||||
isReservationManualReviewFieldWritable,
|
||||
isSelectField,
|
||||
validateReservationFieldValues,
|
||||
} from '@/utils/reservationFieldRules'
|
||||
|
||||
@@ -90,7 +93,7 @@ describe('validateReservationFieldValues', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('groups only fields that match the current display condition', () => {
|
||||
it('groups all backend-returned fields without rebuilding display-condition logic', () => {
|
||||
const groups = groupReservationFields(
|
||||
[
|
||||
createField({
|
||||
@@ -111,10 +114,13 @@ describe('validateReservationFieldValues', () => {
|
||||
},
|
||||
)
|
||||
|
||||
expect(groups.flatMap((group) => group.fields.map((field) => field.field_path))).toEqual(['case_keys.group_code'])
|
||||
expect(groups.flatMap((group) => group.fields.map((field) => field.field_path))).toEqual([
|
||||
'case_keys.group_code',
|
||||
'case_keys.confirmation_number',
|
||||
])
|
||||
})
|
||||
|
||||
it('builds a backend-safe editable payload without readonly or inactive fields', () => {
|
||||
it('builds a backend-safe editable payload without readonly or omitted fields', () => {
|
||||
const payload = buildEditableFieldValues(
|
||||
[
|
||||
createField({
|
||||
@@ -129,16 +135,11 @@ describe('validateReservationFieldValues', () => {
|
||||
display_name: 'Group Code',
|
||||
display_condition: '对象为Group Block或Allotment时展示',
|
||||
}),
|
||||
createField({
|
||||
field_path: 'case_keys.confirmation_number',
|
||||
display_name: 'Confirmation No.',
|
||||
display_condition: '对象为FIT Reservation时展示',
|
||||
}),
|
||||
],
|
||||
{
|
||||
visible_reason: 'AI extracted this task.',
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
'case_keys.confirmation_number': '',
|
||||
'case_keys.confirmation_number': 'CNF-NOT-RETURNED',
|
||||
'extracted_fields.booking_object_type': 'Group Block',
|
||||
},
|
||||
false,
|
||||
@@ -149,6 +150,271 @@ describe('validateReservationFieldValues', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('treats detail.fields as the authoritative backend-filtered field set', () => {
|
||||
const fields = [
|
||||
createField({
|
||||
field_path: 'extracted_fields.booking_object_type',
|
||||
display_name: '订单对象类型',
|
||||
select_editable: 'Y',
|
||||
input_editable: 'N',
|
||||
enum_options: 'Group Block, FIT Reservation',
|
||||
value: 'Group Block',
|
||||
}),
|
||||
createField({
|
||||
field_path: 'case_keys.group_code',
|
||||
display_name: 'Group Code',
|
||||
value: 'GRP-001',
|
||||
}),
|
||||
]
|
||||
const values = buildInitialFieldValues(fields, null, '20260711-p0')
|
||||
const groups = groupReservationFields(fields, '基础信息', {
|
||||
...values,
|
||||
'case_keys.confirmation_number': 'CNF-SHOULD-NOT-RENDER',
|
||||
attachments: [
|
||||
{
|
||||
name: 'hidden-attachment.pdf',
|
||||
},
|
||||
],
|
||||
})
|
||||
const payload = buildEditableFieldValues(
|
||||
fields,
|
||||
{
|
||||
...values,
|
||||
'case_keys.confirmation_number': 'CNF-SHOULD-NOT-SUBMIT',
|
||||
attachments: [
|
||||
{
|
||||
name: 'hidden-attachment.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
expect(groups.flatMap((group) => group.fields.map((field) => field.field_path))).toEqual([
|
||||
'extracted_fields.booking_object_type',
|
||||
'case_keys.group_code',
|
||||
])
|
||||
expect(payload).toEqual({
|
||||
'extracted_fields.booking_object_type': 'Group Block',
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
})
|
||||
expect(payload).not.toHaveProperty('case_keys.confirmation_number')
|
||||
expect(payload).not.toHaveProperty('attachments')
|
||||
})
|
||||
|
||||
it('uses field control metadata before legacy editable flags when building payloads', () => {
|
||||
const payload = buildEditableFieldValues(
|
||||
[
|
||||
createField({
|
||||
field_path: 'case_keys.group_code',
|
||||
display_name: 'Group Code',
|
||||
value: 'GRP-001',
|
||||
control_type: 'text',
|
||||
edit_scope: 'normal_and_manual_review',
|
||||
write_target: 'field_values',
|
||||
raw_readonly: false,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
createField({
|
||||
field_path: 'source_message.attachments',
|
||||
display_name: '来源附件',
|
||||
value: [{ name: 'rooming-list.pdf' }],
|
||||
editable: 'Y',
|
||||
input_editable: 'Y',
|
||||
control_type: 'file',
|
||||
edit_scope: 'never',
|
||||
write_target: 'none',
|
||||
raw_readonly: true,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
createField({
|
||||
field_path: 'route_code',
|
||||
display_name: '路由代码',
|
||||
value: 'R01_NEW_BOOKING_NORMAL',
|
||||
editable: 'Y',
|
||||
input_editable: 'Y',
|
||||
control_type: 'workflow_state',
|
||||
edit_scope: 'system_only',
|
||||
write_target: 'none',
|
||||
raw_readonly: false,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
],
|
||||
{
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
'source_message.attachments': [{ name: 'rooming-list.pdf' }],
|
||||
route_code: 'R01_NEW_BOOKING_NORMAL',
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
expect(payload).toEqual({
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
})
|
||||
})
|
||||
|
||||
it('submits dual draft and review fields in normal editable payloads', () => {
|
||||
const payload = buildEditableFieldValues(
|
||||
[
|
||||
createField({
|
||||
field_path: 'case_keys.group_code',
|
||||
display_name: 'Group Code',
|
||||
value: 'GRP-001',
|
||||
control_type: 'text',
|
||||
edit_scope: 'normal_and_manual_review',
|
||||
write_target: 'draft_payload_and_review_resolution',
|
||||
raw_readonly: false,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
],
|
||||
{
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
expect(payload).toEqual({
|
||||
'case_keys.group_code': 'GRP-001',
|
||||
})
|
||||
})
|
||||
|
||||
it('allows manual review overrides only for fields opened by backend metadata', () => {
|
||||
expect(
|
||||
isReservationManualReviewFieldWritable(
|
||||
createField({
|
||||
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
||||
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
||||
control_type: 'select',
|
||||
edit_scope: 'manual_review_only',
|
||||
write_target: 'review_resolution.field_overrides',
|
||||
raw_readonly: false,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isReservationManualReviewFieldWritable(
|
||||
createField({
|
||||
field_path: 'case_keys.group_code',
|
||||
field_pointer: '/case_keys/group_code',
|
||||
control_type: 'text',
|
||||
edit_scope: 'normal_and_manual_review',
|
||||
write_target: 'draft_payload_and_review_resolution',
|
||||
raw_readonly: false,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isReservationManualReviewFieldWritable(
|
||||
createField({
|
||||
field_path: 'route_code',
|
||||
control_type: 'workflow_state',
|
||||
edit_scope: 'system_only',
|
||||
write_target: 'none',
|
||||
raw_readonly: false,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
isReservationManualReviewFieldWritable(
|
||||
createField({
|
||||
field_path: 'source_message.attachments',
|
||||
control_type: 'file',
|
||||
edit_scope: 'never',
|
||||
write_target: 'none',
|
||||
raw_readonly: true,
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not render review-resolution-only fields as normal editable controls', () => {
|
||||
expect(
|
||||
isReservationFieldEditable(
|
||||
createField({
|
||||
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
||||
control_type: 'select',
|
||||
edit_scope: 'manual_review_only',
|
||||
write_target: 'review_resolution.field_overrides',
|
||||
enum_options: 'RM1, RM2',
|
||||
raw_readonly: false,
|
||||
}),
|
||||
false,
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
isReservationManualReviewFieldWritable(
|
||||
createField({
|
||||
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
||||
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
||||
control_type: 'select',
|
||||
edit_scope: 'manual_review_only',
|
||||
write_target: 'review_resolution.field_overrides',
|
||||
enum_options: 'RM1, RM2',
|
||||
raw_readonly: false,
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat catalog-backed select metadata as a static enum select', () => {
|
||||
const catalogField = createField({
|
||||
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
||||
control_type: 'select',
|
||||
edit_scope: 'manual_review_only',
|
||||
write_target: 'review_resolution.field_overrides',
|
||||
options_source: 'active_pms_room_type_catalog',
|
||||
enum_options: 'Q1A, DLX',
|
||||
raw_readonly: false,
|
||||
})
|
||||
const staticField = createField({
|
||||
field_path: 'extracted_fields.payment_method',
|
||||
control_type: 'select',
|
||||
edit_scope: 'normal_task',
|
||||
write_target: 'draft_payload.field_values',
|
||||
options_source: 'static_enum',
|
||||
enum_options: 'PREPAID, HOTEL_COLLECT',
|
||||
raw_readonly: false,
|
||||
})
|
||||
|
||||
expect(isSelectField(catalogField)).toBe(false)
|
||||
expect(isReservationManualReviewFieldWritable(catalogField)).toBe(true)
|
||||
expect(isSelectField(staticField)).toBe(true)
|
||||
})
|
||||
|
||||
it('honors edit scope white lists for normal payload and manual review writes', () => {
|
||||
const manualOnlyPayloadField = createField({
|
||||
field_path: 'case_keys.confirmation_number',
|
||||
control_type: 'text',
|
||||
edit_scope: 'manual_review_only',
|
||||
write_target: 'field_values',
|
||||
raw_readonly: false,
|
||||
})
|
||||
const workflowDualTargetField = createField({
|
||||
field_path: 'review_status',
|
||||
control_type: 'text',
|
||||
edit_scope: 'workflow_only',
|
||||
write_target: 'draft_payload_and_review_resolution',
|
||||
raw_readonly: false,
|
||||
})
|
||||
const normalOnlyReviewTargetField = createField({
|
||||
field_path: 'case_keys.group_code',
|
||||
control_type: 'text',
|
||||
edit_scope: 'normal_task',
|
||||
write_target: 'review_resolution.field_overrides',
|
||||
raw_readonly: false,
|
||||
})
|
||||
|
||||
expect(isReservationFieldEditable(manualOnlyPayloadField, false)).toBe(false)
|
||||
expect(isReservationFieldEditable(workflowDualTargetField, false)).toBe(false)
|
||||
expect(isReservationManualReviewFieldWritable(manualOnlyPayloadField)).toBe(false)
|
||||
expect(isReservationManualReviewFieldWritable(workflowDualTargetField)).toBe(false)
|
||||
expect(isReservationManualReviewFieldWritable(normalOnlyReviewTargetField)).toBe(false)
|
||||
expect(buildEditableFieldValues(
|
||||
[manualOnlyPayloadField, workflowDualTargetField],
|
||||
{
|
||||
'case_keys.confirmation_number': 'CNF-001',
|
||||
review_status: 'PENDING',
|
||||
},
|
||||
false,
|
||||
)).toEqual({})
|
||||
})
|
||||
|
||||
it('hydrates P0 room_items fields from field_values and submits main field_path keys', () => {
|
||||
const roomTypeField = createField({
|
||||
field_path: 'extracted_fields.room_items.0.room_type_raw',
|
||||
|
||||
@@ -516,12 +516,6 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
display_condition: '对象为Group Block或Allotment时展示',
|
||||
value: 'GRP-001',
|
||||
}),
|
||||
createRequiredField({
|
||||
field_path: 'case_keys.confirmation_number',
|
||||
display_name: 'Confirmation No.',
|
||||
display_condition: '对象为FIT Reservation时展示',
|
||||
value: null,
|
||||
}),
|
||||
createRequiredField({
|
||||
field_path: 'extracted_fields.booking_object_type',
|
||||
display_name: '订单对象类型',
|
||||
@@ -550,6 +544,100 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('renders a CP9 group block detail without assuming FIT or attachment fields exist', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
field_contract_version: '20260711-p0',
|
||||
ai_task_type: 'New Booking',
|
||||
task_subtype: 'new_group_block',
|
||||
fields: [
|
||||
createRequiredField({
|
||||
field_path: 'extracted_fields.booking_object_type',
|
||||
display_name: '订单对象类型',
|
||||
select_editable: 'Y',
|
||||
input_editable: 'N',
|
||||
enum_options: 'Group Block, FIT Reservation',
|
||||
value: 'Group Block',
|
||||
}),
|
||||
createRequiredField({
|
||||
field_path: 'case_keys.group_code',
|
||||
field_pointer: '/case_keys/group_code',
|
||||
display_name: 'Group Code',
|
||||
value: 'GRP-CP9',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
await findButton(wrapper, '保存草稿').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Group Code')
|
||||
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('GRP-CP9')
|
||||
expect(wrapper.text()).not.toContain('Confirmation No.')
|
||||
expect(wrapper.text()).not.toContain('相关附件')
|
||||
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith('10001', {
|
||||
field_values: {
|
||||
'extracted_fields.booking_object_type': 'Group Block',
|
||||
'case_keys.group_code': 'GRP-CP9',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('saves drafts only with editable fields opened by backend control metadata', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
fields: [
|
||||
createRequiredField({
|
||||
field_path: 'case_keys.group_code',
|
||||
field_pointer: '/case_keys/group_code',
|
||||
display_name: 'Group Code',
|
||||
value: 'GRP-OPEN',
|
||||
control_type: 'text',
|
||||
edit_scope: 'normal_and_manual_review',
|
||||
write_target: 'field_values',
|
||||
raw_readonly: false,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
createRequiredField({
|
||||
field_path: 'route_code',
|
||||
display_name: '路由代码',
|
||||
value: 'R01_NEW_BOOKING_NORMAL',
|
||||
editable: 'Y',
|
||||
input_editable: 'Y',
|
||||
control_type: 'workflow_state',
|
||||
edit_scope: 'system_only',
|
||||
write_target: 'none',
|
||||
raw_readonly: false,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
createRequiredField({
|
||||
field_path: 'source_message.attachments',
|
||||
display_name: '来源附件',
|
||||
value: [{ name: 'rooming-list.pdf' }],
|
||||
editable: 'Y',
|
||||
input_editable: 'Y',
|
||||
control_type: 'file',
|
||||
edit_scope: 'never',
|
||||
write_target: 'none',
|
||||
raw_readonly: true,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
],
|
||||
}),
|
||||
)
|
||||
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
await findButton(wrapper, '保存草稿').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith('10001', {
|
||||
field_values: {
|
||||
'case_keys.group_code': 'GRP-OPEN',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('opens the source conversation by source message id without external conversation id', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
@@ -749,6 +837,54 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
expect(wrapper.text()).toContain('人工复核已解阻')
|
||||
})
|
||||
|
||||
it('shows missing manual review fields as backend unopened when detail.fields omits them', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
result_type: 'manual_review',
|
||||
ai_task_type: 'NEW_BOOKING',
|
||||
task_subtype: 'NEW_BOOKING',
|
||||
route_code: 'NEW_BOOKING_MANUAL_REVIEW',
|
||||
system_process_category: 'BUSINESS_TASK',
|
||||
task_status: 'BLOCKED',
|
||||
review_status: 'PENDING',
|
||||
manual_review: {
|
||||
reason_code: 'missing_room_type_code',
|
||||
missing_fields: [
|
||||
'/extracted_fields/room_items/0/pms_room_type_code',
|
||||
'/case_keys/confirmation_number',
|
||||
],
|
||||
review_instruction: '请补充缺失字段。',
|
||||
},
|
||||
availability: {
|
||||
blocked: true,
|
||||
read_only: true,
|
||||
editable: false,
|
||||
confirmable: false,
|
||||
executable: false,
|
||||
blocked_by_task_id: null,
|
||||
blocked_reason: 'MANUAL_REVIEW_REQUIRED',
|
||||
},
|
||||
fields: [
|
||||
createRequiredField({
|
||||
field_path: 'extracted_fields.room_items.0.pms_room_type_code',
|
||||
field_pointer: '/extracted_fields/room_items/0/pms_room_type_code',
|
||||
legacy_field_path: 'extracted_fields.pms_room_type_code',
|
||||
display_name: 'PMS 房型代码',
|
||||
value: null,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
expect(wrapper.text()).toContain('PMS 房型代码')
|
||||
expect(wrapper.text()).toContain('/case_keys/confirmation_number')
|
||||
expect(wrapper.text()).toContain('当前字段后端未开放编辑')
|
||||
expect(wrapper.findAll('input[name^="manual_resolution_field_override_"]')).toHaveLength(1)
|
||||
expect(findButton(wrapper, '提交复核结果').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('resolves type-known manual review on the same task card', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail)
|
||||
.mockResolvedValueOnce(
|
||||
|
||||
Reference in New Issue
Block a user