Files
Wyndham-RSVN/client/src/utils/reservationFieldRules.ts
T

322 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReservationRecord, ReservationTaskFieldResult } from '@/types/reservation'
const truthyRuleValues = new Set(['Y', 'YES', 'TRUE', '1', '是', '可编辑', 'ENABLED'])
const falsyRuleValues = new Set(['N', 'NO', 'FALSE', '0', '否', '不可编辑', 'DISABLED', '隐藏'])
export interface FieldAreaGroup {
area: string
fields: ReservationTaskFieldResult[]
}
export interface EnumOption {
value: string
label: string
}
export type ReservationFieldValidationErrorCode = 'required' | 'enum' | 'date' | 'number'
export interface ReservationFieldValidationError {
code: ReservationFieldValidationErrorCode
field_name: string
}
export type ReservationFieldValidationErrors = Record<string, ReservationFieldValidationError>
export function groupReservationFields(
fields: ReservationTaskFieldResult[],
fallbackArea: string,
values: ReservationRecord = {},
): FieldAreaGroup[] {
const grouped = new Map<string, ReservationTaskFieldResult[]>()
fields.filter((field) => isReservationFieldActive(field, values)).forEach((field) => {
const area = field.display_area?.trim() || fallbackArea
grouped.set(area, [...(grouped.get(area) ?? []), field])
})
return Array.from(grouped.entries()).map(([area, areaFields]) => ({
area,
fields: areaFields.sort((left, right) => (left.row_number ?? 0) - (right.row_number ?? 0)),
}))
}
export function isReservationFieldVisible(field: ReservationTaskFieldResult): boolean {
return !isRuleFalsy(field.visible)
}
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
}
export function isReservationFieldRequired(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.required_rule)
}
export function isReservationFieldEditable(field: ReservationTaskFieldResult, readOnly: boolean): boolean {
if (readOnly) {
return false
}
return (
isRuleTruthy(field.editable) ||
isRuleTruthy(field.input_editable) ||
isRuleTruthy(field.select_editable) ||
isRuleTruthy(field.date_picker) ||
isRuleTruthy(field.number_input) ||
isRuleTruthy(field.table_editable)
)
}
export function isSelectField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.select_editable) && parseEnumOptions(field.enum_options).length > 0
}
export function isDateField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.date_picker)
}
export function isNumberField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.number_input)
}
export function isFileField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.file_display)
}
export function isTableField(field: ReservationTaskFieldResult): boolean {
return isRuleTruthy(field.table_editable)
}
export function parseEnumOptions(enumOptions: string | null): EnumOption[] {
if (!enumOptions?.trim()) {
return []
}
return enumOptions
.split(/[,,;;/]/)
.map((item) => item.trim())
.filter(Boolean)
.map((item) => {
const separatorIndex = item.indexOf(':')
const value = separatorIndex >= 0 ? item.slice(0, separatorIndex) : item
const label = separatorIndex >= 0 ? item.slice(separatorIndex + 1) : value
return {
value: value.trim(),
label: label.trim(),
}
})
}
export function buildInitialFieldValues(fields: ReservationTaskFieldResult[], payload: ReservationRecord | null): ReservationRecord {
return fields.reduce<ReservationRecord>((values, field) => {
values[field.field_path] = payload?.[field.field_path] ?? field.value ?? ''
return values
}, {})
}
export function buildEditableFieldValues(
fields: ReservationTaskFieldResult[],
values: ReservationRecord,
readOnly: boolean,
): ReservationRecord {
if (readOnly) {
return {}
}
return fields
.filter((field) => isReservationFieldActive(field, values))
.filter((field) => isReservationFieldEditable(field, readOnly))
.reduce<ReservationRecord>((payload, field) => {
payload[field.field_path] = field.field_path in values ? values[field.field_path] : field.value ?? ''
return payload
}, {})
}
export function validateReservationFieldValues(
fields: ReservationTaskFieldResult[],
values: ReservationRecord,
): ReservationFieldValidationErrors {
return fields.filter((field) => isReservationFieldActive(field, values)).reduce<ReservationFieldValidationErrors>((errors, field) => {
const value = values[field.field_path] ?? field.value ?? ''
if (isReservationFieldRequired(field) && isEmptyReservationValue(value)) {
errors[field.field_path] = createValidationError(field, 'required')
return errors
}
if (isEmptyReservationValue(value)) {
return errors
}
if (isSelectField(field) && !parseEnumOptions(field.enum_options).some((option) => option.value === String(value))) {
errors[field.field_path] = createValidationError(field, 'enum')
return errors
}
if (isDateField(field) && !isIsoLocalDate(String(value))) {
errors[field.field_path] = createValidationError(field, 'date')
return errors
}
if (isNumberField(field) && !Number.isFinite(Number(value))) {
errors[field.field_path] = createValidationError(field, 'number')
}
return errors
}, {})
}
function valueForConditionPath(
values: ReservationRecord,
currentField: ReservationTaskFieldResult,
fieldPath: string,
): unknown {
if (fieldPath in values) {
return values[fieldPath]
}
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,
): ReservationFieldValidationError {
return {
code,
field_name: field.display_name,
}
}
export function stringifyReservationValue(value: unknown, emptyLabel: string): string {
if (value === null || value === undefined || value === '') {
return emptyLabel
}
if (Array.isArray(value)) {
return value.map((item) => stringifyReservationValue(item, emptyLabel)).join(', ')
}
if (typeof value === 'object') {
return JSON.stringify(value)
}
return String(value)
}
function isEmptyReservationValue(value: unknown): boolean {
if (value === null || value === undefined) {
return true
}
if (typeof value === 'string') {
return value.trim() === ''
}
if (Array.isArray(value)) {
return value.length === 0
}
return false
}
function isIsoLocalDate(value: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return false
}
const date = new Date(`${value}T00:00:00Z`)
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
}
function isRuleTruthy(value: string | null): boolean {
if (!value) {
return false
}
return truthyRuleValues.has(value.trim().toUpperCase()) || truthyRuleValues.has(value.trim())
}
function isRuleFalsy(value: string | null): boolean {
if (!value) {
return false
}
return falsyRuleValues.has(value.trim().toUpperCase()) || falsyRuleValues.has(value.trim())
}