接入前端真实接口并标记邮件HTML清洗
This commit is contained in:
@@ -13,9 +13,22 @@ export interface EnumOption {
|
||||
label: string
|
||||
}
|
||||
|
||||
export function groupReservationFields(fields: ReservationTaskFieldResult[], fallbackArea: string): FieldAreaGroup[] {
|
||||
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(isReservationFieldVisible).forEach((field) => {
|
||||
fields.filter((field) => isReservationFieldActive(field, values)).forEach((field) => {
|
||||
const area = field.display_area?.trim() || fallbackArea
|
||||
grouped.set(area, [...(grouped.get(area) ?? []), field])
|
||||
})
|
||||
@@ -30,6 +43,71 @@ export function isReservationFieldVisible(field: ReservationTaskFieldResult): bo
|
||||
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)
|
||||
}
|
||||
@@ -95,6 +173,105 @@ export function buildInitialFieldValues(fields: ReservationTaskFieldResult[], pa
|
||||
}, {})
|
||||
}
|
||||
|
||||
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
|
||||
@@ -108,6 +285,27 @@ export function stringifyReservationValue(value: unknown, emptyLabel: string): s
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user