接入M002V4订单任务多卡前端

This commit is contained in:
andy
2026-07-19 13:48:07 +07:00
parent 13364e7edf
commit cae169cefd
19 changed files with 4084 additions and 343 deletions

View File

@@ -0,0 +1,397 @@
import type {
ReservationRecord,
ReservationV4ReviewFieldOverrideRequest,
ReservationV4TaskCardFieldResult,
} from '@/types/reservation'
const confirmEditScopes = new Set(['NORMAL_TASK', 'NORMAL_AND_MANUAL_REVIEW', 'CONFIRM'])
const reviewEditScopes = new Set(['MANUAL_REVIEW_ONLY', 'NORMAL_AND_MANUAL_REVIEW', 'REVIEW'])
const confirmWriteTargets = new Set([
'CONFIRMED_PAYLOAD_JSON',
'CONFIRMED_PAYLOAD',
'CONFIRMED_PAYLOAD.FIELD_VALUES',
'FIELD_VALUES',
])
const reviewWriteTargets = new Set([
'REVIEW_RESOLUTION_FIELD_OVERRIDES',
'REVIEW_RESOLUTION.FIELD_OVERRIDES',
'FIELD_OVERRIDES',
])
const readonlyControlTypes = new Set(['READONLY', 'FILE', 'WORKFLOW_STATE', 'STRUCTURED_TABLE'])
const reviewWritablePointerPrefixes = ['/basic_information/', '/business_fields/']
const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
export type ReservationV4FieldErrorMap = Record<string, string>
export function reservationV4FieldKey(field: ReservationV4TaskCardFieldResult): string {
return field.field_pointer || field.field_path
}
export function buildReservationV4InitialFieldValues(
fields: ReservationV4TaskCardFieldResult[],
): ReservationRecord {
return fields.reduce<ReservationRecord>((values, field) => {
values[reservationV4FieldKey(field)] = field.value ?? ''
return values
}, {})
}
export function buildReservationV4ConfirmedPayload(
fields: ReservationV4TaskCardFieldResult[],
values: ReservationRecord,
): ReservationRecord {
return fields
.filter(isReservationV4ConfirmWritableField)
.reduce<ReservationRecord>((payload, field) => {
assignByField(payload, field, readV4FieldValue(field, values))
return payload
}, {})
}
export function buildReservationV4ReviewOverrides(
fields: ReservationV4TaskCardFieldResult[],
values: ReservationRecord,
): ReservationV4ReviewFieldOverrideRequest[] {
return fields
.filter(isReservationV4ReviewWritableField)
.map((field) => ({
field_pointer: field.field_pointer || fieldPathToPointer(field.field_path),
value: readV4FieldValue(field, values),
}))
}
export function validateReservationV4Fields(
fields: ReservationV4TaskCardFieldResult[],
values: ReservationRecord,
): ReservationV4FieldErrorMap {
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
const key = reservationV4FieldKey(field)
if (field.required && isEmptyV4Value(readV4FieldValue(field, values))) {
errors[key] = `${field.display_name} is required`
}
return errors
}, {})
}
export function isReservationV4ConfirmWritableField(field: ReservationV4TaskCardFieldResult): boolean {
if (!isReservationV4EditableField(field)) {
return false
}
if (isUnsafeV4Field(field)) {
return false
}
const editScope = normalizeV4RuleCode(field.edit_scope)
if (!confirmEditScopes.has(editScope)) {
return false
}
return confirmWriteTargets.has(normalizeV4RuleCode(field.write_target))
}
export function isReservationV4ReviewWritableField(field: ReservationV4TaskCardFieldResult): boolean {
if (!isReservationV4EditableField(field)) {
return false
}
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
if (!reviewWritablePointerPrefixes.some((prefix) => pointer.startsWith(prefix))) {
return false
}
if (isUnsafeV4Field(field)) {
return false
}
const editScope = normalizeV4RuleCode(field.edit_scope)
if (!reviewEditScopes.has(editScope)) {
return false
}
return reviewWriteTargets.has(normalizeV4RuleCode(field.write_target))
}
export function isReservationV4EditableField(field: ReservationV4TaskCardFieldResult): boolean {
if (field.editable !== true || field.raw_readonly === true) {
return false
}
if (normalizeV4RuleCode(field.write_target) === 'NONE') {
return false
}
return !readonlyControlTypes.has(normalizeV4RuleCode(field.control_type))
}
export function readV4FieldValue(
field: ReservationV4TaskCardFieldResult,
values: ReservationRecord,
): unknown {
const pointer = field.field_pointer ? values[field.field_pointer] : undefined
if (pointer !== undefined) {
return pointer
}
const path = field.field_path ? values[field.field_path] : undefined
return path !== undefined ? path : field.value ?? ''
}
export function fieldPathToPointer(fieldPath: string): string {
if (!fieldPath) {
return ''
}
return `/${fieldPath.split('.').map(encodePointerSegment).join('/')}`
}
export function mapReservationV4BackendDetailsToFields(
details: unknown,
fields: ReservationV4TaskCardFieldResult[],
): {
fieldErrors: ReservationV4FieldErrorMap
globalErrors: string[]
} {
const messages = normalizeBackendDetailMessages(details)
const fieldErrors: ReservationV4FieldErrorMap = {}
const globalErrors: string[] = []
for (const message of messages) {
const matchedField = fields.find((field) =>
message.includes(field.field_pointer) ||
Boolean(field.field_path && message.includes(field.field_path)),
)
if (matchedField) {
fieldErrors[reservationV4FieldKey(matchedField)] = message
} else {
globalErrors.push(message)
}
}
return {
fieldErrors,
globalErrors,
}
}
export function staticReservationV4Options(optionsSource: string | null): Array<{ value: string; label: string }> {
switch (normalizeV4RuleCode(optionsSource)) {
case 'RESERVATION_V4_ACCOUNT_CATALOG':
case 'ACCOUNT_CATALOG':
return [
{ value: 'HANATOUR', label: 'HANATOUR' },
{ value: 'LIAN_TAI', label: 'LIAN TAI' },
{ value: 'QBD_TRAVEL', label: 'QBD Travel' },
]
case 'RESERVATION_V4_ROOM_TYPE_CATALOG':
case 'ROOM_TYPE_CATALOG':
return [
{ value: 'TWN', label: 'TWN' },
{ value: 'DBL', label: 'DBL' },
{ value: 'KING', label: 'KING' },
{ value: 'SGL', label: 'SGL' },
{ value: 'TRP', label: 'TRP' },
]
case 'RESERVATION_V4_RATE_CODE_CATALOG':
case 'RATE_CODE_CATALOG':
return [
{ value: 'BAR', label: 'BAR' },
{ value: 'RACK', label: 'RACK' },
{ value: 'GROUP', label: 'GROUP' },
{ value: 'FIT', label: 'FIT' },
]
default:
return []
}
}
export function isReservationV4UnsafeDisplayKey(key: string): boolean {
const normalizedKey = key.toLowerCase()
return [
'raw',
'html_body',
'body',
'agentbus',
'payload',
'url',
'oss',
'evidence',
'attachment',
'file_reference',
].some((fragment) => normalizedKey.includes(fragment))
}
export function stringifyReservationV4SafeDisplayValue(
value: unknown,
emptyLabel: string,
hiddenLabel: string,
): string {
const sanitizedValue = sanitizeReservationV4DisplayValue(value)
if (sanitizedValue === hiddenDisplayValue) {
return hiddenLabel
}
if (isEmptyV4Value(sanitizedValue)) {
return emptyLabel
}
if (Array.isArray(sanitizedValue)) {
return sanitizedValue.length ? JSON.stringify(sanitizedValue) : emptyLabel
}
if (isRecord(sanitizedValue)) {
return Object.keys(sanitizedValue).length ? JSON.stringify(sanitizedValue) : hiddenLabel
}
return String(sanitizedValue)
}
function assignByField(
payload: ReservationRecord,
field: ReservationV4TaskCardFieldResult,
value: unknown,
): void {
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
assignByPointer(payload, pointer, value)
}
function assignByPointer(target: ReservationRecord, pointer: string, value: unknown): void {
const segments = decodePointer(pointer)
if (!segments.length) {
return
}
let current: ReservationRecord | unknown[] = target
segments.forEach((segment, index) => {
const isLast = index === segments.length - 1
if (isLast) {
setContainerValue(current, segment, value)
return
}
const nextSegment = segments[index + 1] ?? ''
const nextContainer = readContainerValue(current, segment)
if (isRecord(nextContainer) || Array.isArray(nextContainer)) {
current = nextContainer
return
}
const createdContainer: ReservationRecord | unknown[] = isNumericSegment(nextSegment) ? [] : {}
setContainerValue(current, segment, createdContainer)
current = createdContainer
})
}
function readContainerValue(container: ReservationRecord | unknown[], segment: string): unknown {
if (Array.isArray(container)) {
return container[Number(segment)]
}
return container[segment]
}
function setContainerValue(container: ReservationRecord | unknown[], segment: string, value: unknown): void {
if (Array.isArray(container)) {
container[Number(segment)] = value
return
}
container[segment] = value
}
function decodePointer(pointer: string): string[] {
if (!pointer || pointer === '/') {
return []
}
return pointer
.replace(/^\//, '')
.split('/')
.filter(Boolean)
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
}
function encodePointerSegment(segment: string): string {
return segment.replace(/~/g, '~0').replace(/\//g, '~1')
}
function normalizeBackendDetailMessages(details: unknown): string[] {
if (details === null || details === undefined || details === '') {
return []
}
if (Array.isArray(details)) {
return details.flatMap((item) => normalizeBackendDetailMessages(item))
}
if (typeof details === 'string') {
return [details]
}
if (typeof details === 'object') {
const record = details as ReservationRecord
const path = readFirstDetailString(record, ['field_path', 'field_pointer', 'field', 'path', 'json_pointer'])
const message = typeof record.message === 'string'
? record.message
: typeof record.error_message === 'string'
? record.error_message
: JSON.stringify(record)
return [`${path ? `${path}: ` : ''}${message}`]
}
return [String(details)]
}
function readFirstDetailString(record: ReservationRecord, keys: string[]): string {
for (const key of keys) {
const value = record[key]
if (typeof value === 'string' && value.trim()) {
return value.trim()
}
}
return ''
}
function sanitizeReservationV4DisplayValue(value: unknown): unknown {
if (value === null || value === undefined || value === '') {
return value
}
if (typeof value === 'string') {
return isUnsafeUrlLikeValue(value) ? hiddenDisplayValue : value
}
if (Array.isArray(value)) {
if (!value.length) {
return value
}
const sanitizedItems = value
.map((item) => sanitizeReservationV4DisplayValue(item))
.filter((item) => item !== hiddenDisplayValue)
return sanitizedItems.length ? sanitizedItems : hiddenDisplayValue
}
if (isRecord(value)) {
const sanitizedRecord = Object.entries(value).reduce<ReservationRecord>((record, [key, nestedValue]) => {
if (isReservationV4UnsafeDisplayKey(key)) {
return record
}
const sanitizedValue = sanitizeReservationV4DisplayValue(nestedValue)
if (sanitizedValue !== hiddenDisplayValue) {
record[key] = sanitizedValue
}
return record
}, {})
return Object.keys(sanitizedRecord).length ? sanitizedRecord : hiddenDisplayValue
}
return value
}
function isUnsafeV4Field(field: ReservationV4TaskCardFieldResult): boolean {
return isReservationV4UnsafeDisplayKey(field.field_path) || isReservationV4UnsafeDisplayKey(field.field_pointer)
}
function isUnsafeUrlLikeValue(value: string): boolean {
const trimmedValue = value.trim()
return /^(https?:\/\/|oss:\/\/|s3:\/\/)/i.test(trimmedValue)
}
function normalizeV4RuleCode(value: string | null | undefined): string {
return value?.trim().replace(/[.\s-]+/g, '_').toUpperCase() ?? ''
}
function isEmptyV4Value(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 isNumericSegment(segment: string): boolean {
return /^\d+$/.test(segment)
}
function isRecord(value: unknown): value is ReservationRecord {
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
}