修复任务字段过滤与控件可写规则

This commit is contained in:
andy
2026-07-13 09:52:10 +08:00
parent b05001e481
commit 20eac61cb6
9 changed files with 593 additions and 132 deletions

View File

@@ -2,6 +2,23 @@ import type { ReservationRecord, ReservationTaskFieldResult } from '@/types/rese
const truthyRuleValues = new Set(['Y', 'YES', 'TRUE', '1', '是', '可编辑', 'ENABLED'])
const falsyRuleValues = new Set(['N', 'NO', 'FALSE', '0', '否', '不可编辑', 'DISABLED', '隐藏'])
const normalPayloadEditScopes = new Set(['NORMAL_TASK', 'NORMAL_AND_MANUAL_REVIEW'])
const manualReviewEditScopes = new Set(['MANUAL_REVIEW_ONLY', 'NORMAL_AND_MANUAL_REVIEW'])
const editableControlTypes = new Set(['TEXT', 'TEXTAREA', 'INPUT', 'SELECT', 'DATE', 'NUMBER'])
const readonlyControlTypes = new Set(['READONLY', 'FILE', 'WORKFLOW_STATE', 'STRUCTURED_TABLE'])
const staticOptionSources = new Set(['STATIC_ENUM'])
const payloadWriteTargets = new Set([
'FIELD_VALUES',
'DRAFT_PAYLOAD.FIELD_VALUES',
'CONFIRMED_PAYLOAD.FIELD_VALUES',
'TASK_FIELD_VALUES',
'DRAFT_PAYLOAD_AND_REVIEW_RESOLUTION',
])
const manualReviewWriteTargets = new Set([
'REVIEW_RESOLUTION.FIELD_OVERRIDES',
'FIELD_OVERRIDES',
'DRAFT_PAYLOAD_AND_REVIEW_RESOLUTION',
])
const roomItemFieldPathAliases: Record<string, string[]> = {
'extracted_fields.room_quantity': ['extracted_fields.room_items.0.room_quantity'],
'extracted_fields.room_type': [
@@ -58,68 +75,8 @@ export function isReservationFieldVisible(field: ReservationTaskFieldResult): bo
}
export function isReservationFieldActive(field: ReservationTaskFieldResult, values: ReservationRecord = {}): boolean {
if (!isReservationFieldVisible(field)) {
return false
}
const condition = field.display_condition?.trim()
if (!condition || condition === '-' || condition.includes('始终展示')) {
return true
}
const currentValue = valueForConditionPath(values, field, field.field_path)
if (condition.includes('有值时展示')) {
return !isEmptyReservationValue(currentValue)
}
if (condition.includes('有附件')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'attachments'))
}
if (condition.includes('对象为FIT Reservation')) {
return isFitTarget(values, field)
}
if (condition.includes('对象为Group Block或Allotment')) {
return isGroupOrAllotmentTarget(values, field)
}
if (condition.includes('涉及房型')) {
return (
matrixContains(field.task_type, 'New Booking') ||
!isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.room_type')) ||
!isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.pms_room_type_code'))
)
}
if (condition.includes('涉及Rate Code') || condition.includes('涉及价格/Rate Code')) {
return (
!isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.rate_code_result')) ||
hasNonEmptyValueWithPrefix(values, 'extracted_fields.rate_code_result.') ||
matrixContains(field.task_subtype, 'manual_rate_code_or_settlement_price')
)
}
if (condition.includes('需要价格确认')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.rate_code_result.settlement_price'))
}
if (condition.includes('有复合单价') || condition.includes('fix_charge_required=true')) {
return (
isBooleanTrue(valueForConditionPath(values, field, 'extracted_fields.fix_charge_required')) ||
!isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.fix_charge_items[]'))
)
}
if (condition.includes('allocation_split_from_parent=true')) {
return isBooleanTrue(valueForConditionPath(values, field, 'extracted_fields.allocation_split_from_parent'))
}
if (condition.includes('来自QBD/LianTai表格')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.table_evidence'))
}
if (condition.includes('有Group目标')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'case_keys.group_code')) || isGroupOrAllotmentTarget(values, field)
}
if (condition.includes('有FIT目标') || condition.includes('FIT取消')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'case_keys.confirmation_number')) || isFitTarget(values, field)
}
if (condition.includes('有before/after修改')) {
return !isEmptyReservationValue(valueForConditionPath(values, field, 'extracted_fields.before_after'))
}
if (condition.includes('update_fix_charge')) {
return matrixContains(field.task_subtype, 'update_fix_charge') || isBooleanTrue(valueForConditionPath(values, field, 'extracted_fields.fix_charge_required'))
}
return true
void values
return isReservationFieldVisible(field)
}
export function isReservationFieldRequired(field: ReservationTaskFieldResult): boolean {
@@ -130,6 +87,9 @@ export function isReservationFieldEditable(field: ReservationTaskFieldResult, re
if (readOnly) {
return false
}
if (hasControlMetadata(field)) {
return isReservationFieldEditableByControlMetadata(field)
}
return (
isRuleTruthy(field.editable) ||
isRuleTruthy(field.input_editable) ||
@@ -141,23 +101,28 @@ export function isReservationFieldEditable(field: ReservationTaskFieldResult, re
}
export function isSelectField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.select_editable) && parseEnumOptions(field.enum_options).length > 0
return (
normalizeControlValue(field.control_type) === 'SELECT' ||
isRuleTruthy(field.select_editable)
) &&
isStaticOptionsSource(field) &&
parseEnumOptions(field.enum_options).length > 0
}
export function isDateField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.date_picker)
return normalizeControlValue(field.control_type) === 'DATE' || isRuleTruthy(field.date_picker)
}
export function isNumberField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.number_input)
return normalizeControlValue(field.control_type) === 'NUMBER' || isRuleTruthy(field.number_input)
}
export function isFileField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.file_display)
return normalizeControlValue(field.control_type) === 'FILE' || isRuleTruthy(field.file_display)
}
export function isTableField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.table_editable)
return normalizeControlValue(field.control_type) === 'STRUCTURED_TABLE' || isRuleTruthy(field.table_editable)
}
export function parseEnumOptions(enumOptions: string | null): EnumOption[] {
@@ -201,13 +166,28 @@ export function buildEditableFieldValues(
}
return fields
.filter((field) => isReservationFieldActive(field, values))
.filter((field) => isReservationFieldEditable(field, readOnly))
.filter((field) => isReservationFieldPayloadWritable(field, readOnly))
.reduce<ReservationRecord>((payload, field) => {
payload[field.field_path] = readReservationFieldValue(field, values)
return payload
}, {})
}
export function isReservationManualReviewFieldWritable(field: ReservationTaskFieldResult): boolean {
if (!hasControlMetadata(field)) {
return true
}
if (isRawReadonly(field.raw_readonly)) {
return false
}
const editScope = normalizeControlValue(field.edit_scope)
if (editScope && !manualReviewEditScopes.has(editScope)) {
return false
}
const writeTarget = normalizeControlValue(field.write_target)
return !writeTarget || manualReviewWriteTargets.has(writeTarget)
}
export function validateReservationFieldValues(
fields: ReservationTaskFieldResult[],
values: ReservationRecord,
@@ -241,47 +221,6 @@ export function validateReservationFieldValues(
}, {})
}
function valueForConditionPath(
values: ReservationRecord,
currentField: ReservationTaskFieldResult,
fieldPath: string,
): unknown {
const value = readValueByFieldPath(values, fieldPath)
if (value !== undefined) {
return value
}
if (fieldPath === currentField.field_path) {
return currentField.value
}
return undefined
}
function hasNonEmptyValueWithPrefix(values: ReservationRecord, prefix: string): boolean {
return Object.entries(values).some(([key, value]) => key.startsWith(prefix) && !isEmptyReservationValue(value))
}
function isFitTarget(values: ReservationRecord, field: ReservationTaskFieldResult): boolean {
const target = stringifyConditionValue(valueForConditionPath(values, field, 'extracted_fields.booking_object_type'))
return target.includes('FIT') || target.includes('RESERVATION')
}
function isGroupOrAllotmentTarget(values: ReservationRecord, field: ReservationTaskFieldResult): boolean {
const target = stringifyConditionValue(valueForConditionPath(values, field, 'extracted_fields.booking_object_type'))
return target.includes('GROUP') || target.includes('ALLOTMENT') || !isEmptyReservationValue(valueForConditionPath(values, field, 'case_keys.group_code'))
}
function stringifyConditionValue(value: unknown): string {
return value === null || value === undefined ? '' : String(value).trim().toUpperCase()
}
function matrixContains(value: string | null | undefined, keyword: string): boolean {
return Boolean(value?.toLowerCase().includes(keyword.toLowerCase()))
}
function isBooleanTrue(value: unknown): boolean {
return value === true || String(value).trim().toLowerCase() === 'true' || String(value).trim() === '是'
}
function createValidationError(
field: ReservationTaskFieldResult,
code: ReservationFieldValidationErrorCode,
@@ -369,12 +308,70 @@ export function readReservationFieldValue(
])
}
function readValueByFieldPath(values: ReservationRecord, fieldPath: string): unknown {
const flatValue = firstValueByPaths(values, fieldPathCandidates(fieldPath))
if (flatValue !== undefined) {
return flatValue
function isReservationFieldPayloadWritable(field: ReservationTaskFieldResult, readOnly: boolean): boolean {
if (!isReservationFieldEditable(field, readOnly)) {
return false
}
return readNestedPath(values, fieldPath)
if (!hasControlMetadata(field)) {
return true
}
const writeTarget = normalizeControlValue(field.write_target)
return !writeTarget || payloadWriteTargets.has(writeTarget)
}
function isReservationFieldEditableByControlMetadata(field: ReservationTaskFieldResult): boolean {
if (isRawReadonly(field.raw_readonly)) {
return false
}
const editScope = normalizeControlValue(field.edit_scope)
if (editScope && !normalPayloadEditScopes.has(editScope)) {
return false
}
const writeTarget = normalizeControlValue(field.write_target)
if (writeTarget === 'NONE') {
return false
}
if (writeTarget && !payloadWriteTargets.has(writeTarget)) {
return false
}
const controlType = normalizeControlValue(field.control_type)
if (readonlyControlTypes.has(controlType)) {
return false
}
if (controlType) {
return editableControlTypes.has(controlType)
}
return (
isRuleTruthy(field.editable) ||
isRuleTruthy(field.input_editable) ||
isRuleTruthy(field.select_editable) ||
isRuleTruthy(field.date_picker) ||
isRuleTruthy(field.number_input)
)
}
function isStaticOptionsSource(field: ReservationTaskFieldResult): boolean {
const optionsSource = normalizeControlValue(field.options_source)
return !optionsSource || staticOptionSources.has(optionsSource)
}
function hasControlMetadata(field: ReservationTaskFieldResult): boolean {
return Boolean(
field.control_type ||
field.edit_scope ||
field.write_target ||
field.options_source ||
field.raw_readonly !== undefined && field.raw_readonly !== null ||
field.control_hint,
)
}
function isRawReadonly(value: boolean | string | null | undefined): boolean {
return value === true || String(value).trim().toUpperCase() === 'TRUE'
}
function normalizeControlValue(value: string | null | undefined): string {
return value?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
}
function fieldPathCandidates(fieldPath: string): string[] {