修复任务字段过滤与控件可写规则
This commit is contained in:
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user