提交代码

This commit is contained in:
andy
2026-07-22 15:30:26 +07:00
parent 1236df86cf
commit 7e2cd8969f
7 changed files with 137 additions and 9 deletions

View File

@@ -29,7 +29,7 @@
:class="{ 'trace-field--readonly': !isEditable(field) }"
>
<span class="trace-field__label">
{{ field.display_name }}
{{ traceFieldLabel(field) }}
<sup v-if="field.required">*</sup>
</span>
@@ -387,6 +387,23 @@ function fieldPointer(field: ReservationV4TaskCardFieldResult): string {
return field.field_pointer || fieldPathToPointer(field.field_path)
}
function traceFieldLabel(field: ReservationV4TaskCardFieldResult): string {
const pointer = fieldPointer(field)
if (pointer.endsWith('/text')) {
return t('taskV4.trace.textLabel')
}
if (pointer.endsWith('/department_code')) {
return t('taskV4.trace.department')
}
if (pointer.endsWith('/target_room_type_code')) {
return t('taskV4.trace.targetRoomType')
}
if (pointer.endsWith('/extra_bed_room_count')) {
return t('taskV4.trace.extraBedRoomCount')
}
return field.display_name || fieldKey(field)
}
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
if (item.display_name && item.display_name !== item.code) {
return `${item.code} - ${item.display_name}`

View File

@@ -628,6 +628,10 @@ export default {
cardConfirmed: 'Card confirmed and detail refreshed.',
reviewResolved: 'Review submitted and detail refreshed.',
validationFailed: 'Fix current-card field errors first.',
fieldValidation: {
required: '{field} is required',
positiveInteger: '{field} must be an integer greater than 0',
},
fieldLabels: {
basicInformation: {
accountCode: 'Account Code',
@@ -692,6 +696,10 @@ export default {
generalTitle: 'General note',
extraBedTitle: 'Extra bed note',
action: 'Fixed action',
textLabel: 'Follow-up note',
department: 'Department',
targetRoomType: 'Target room type',
extraBedRoomCount: 'Extra bed room count',
},
lookup: {
loading: 'Loading options',

View File

@@ -628,6 +628,10 @@ export default {
cardConfirmed: 'ยืนยันการ์ดแล้วและรีเฟรชรายละเอียดแล้ว',
reviewResolved: 'ส่งผลตรวจสอบแล้วและรีเฟรชรายละเอียดแล้ว',
validationFailed: 'โปรดแก้ไขข้อมูลในการ์ดปัจจุบันก่อน',
fieldValidation: {
required: 'จำเป็นต้องกรอก {field}',
positiveInteger: '{field} ต้องเป็นจำนวนเต็มมากกว่า 0',
},
fieldLabels: {
basicInformation: {
accountCode: 'รหัสบัญชี',
@@ -692,6 +696,10 @@ export default {
generalTitle: 'หมายเหตุทั่วไป',
extraBedTitle: 'รายการเตียงเสริม',
action: 'การดำเนินการคงที่',
textLabel: 'รายละเอียดติดตาม',
department: 'แผนกที่รับผิดชอบ',
targetRoomType: 'ประเภทห้องเป้าหมาย',
extraBedRoomCount: 'จำนวนห้องที่เพิ่มเตียง',
},
lookup: {
loading: 'กำลังโหลดตัวเลือก',

View File

@@ -627,6 +627,10 @@ export default {
cardConfirmed: '卡片已确认,详情已刷新。',
reviewResolved: '复核已提交,详情已刷新。',
validationFailed: '请先修正当前卡片字段。',
fieldValidation: {
required: '{field}为必填项',
positiveInteger: '{field}必须是大于 0 的整数',
},
fieldLabels: {
basicInformation: {
accountCode: '客户代码',
@@ -691,6 +695,10 @@ export default {
generalTitle: '普通事项',
extraBedTitle: '加床事项',
action: '固定动作',
textLabel: '跟进内容',
department: '处理部门',
targetRoomType: '目标房型',
extraBedRoomCount: '加床房间数',
},
lookup: {
loading: '选项加载中',

View File

@@ -662,6 +662,10 @@ describe('reservation V4 pages', () => {
.findAll('.task-card-section')
.find((section) => section.find('[data-testid="trace-card"]').exists())
expect(traceSection).toBeTruthy()
expect(traceSection!.text()).toContain(zhCN.taskV4.trace.textLabel)
expect(traceSection!.text()).toContain(zhCN.taskV4.trace.department)
expect(traceSection!.text()).not.toContain('Trace 内容')
expect(traceSection!.text()).not.toContain('Department Code')
expect((traceSection!.find('textarea[name="/trace_items/0/text"]').element as HTMLTextAreaElement).value)
.toBe('Late arrival note')
expect(traceSection!.text()).not.toContain('CONTENT_SHOULD_NOT_RENDER')
@@ -716,6 +720,12 @@ describe('reservation V4 pages', () => {
.find((section) => section.find('[data-testid="trace-card"]').exists())
expect(traceSection).toBeTruthy()
expect(traceSection!.text()).toContain('SET EXTRA BED')
expect(traceSection!.text()).toContain(zhCN.taskV4.trace.targetRoomType)
expect(traceSection!.text()).toContain(zhCN.taskV4.trace.extraBedRoomCount)
expect(traceSection!.text()).toContain(zhCN.taskV4.trace.department)
expect(traceSection!.text()).not.toContain('Target Room Type Code')
expect(traceSection!.text()).not.toContain('Extra Bed Room Count')
expect(traceSection!.text()).not.toContain('Department Code')
expect(traceSection!.find('select[name="/trace_items/0/target_room_type_code"]').text()).toContain('RM2')
const roomCountInput = traceSection!.find('input[name="/trace_items/0/extra_bed_room_count"]')
expect(roomCountInput.attributes('type')).toBe('number')
@@ -729,7 +739,10 @@ describe('reservation V4 pages', () => {
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
expect(traceSection!.text()).toContain('Extra Bed Room Count must be a positive integer')
expect(traceSection!.text()).toContain(
zhCN.taskV4.fieldValidation.positiveInteger.replace('{field}', zhCN.taskV4.trace.extraBedRoomCount),
)
expect(traceSection!.text()).not.toContain('Extra Bed Room Count must be a positive integer')
await roomCountInput.setValue('2')
await traceSection!.find('button.primary-button').trigger('click')
@@ -2045,7 +2058,7 @@ function useTraceBusinessCard(
fields: itemType === 'EXTRA_BED'
? [
createField('/trace_items/0/target_room_type_code', {
display_name: 'Target Room Type',
display_name: 'Target Room Type Code',
value: 'RM2',
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
control_type: 'SELECT',
@@ -2070,7 +2083,7 @@ function useTraceBusinessCard(
]
: [
createField('/trace_items/0/text', {
display_name: 'Trace Text',
display_name: 'Trace 内容',
value: 'Late arrival note',
control_type: 'TEXTAREA',
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
@@ -2099,7 +2112,7 @@ function createTraceDepartmentField(
validationErrors: string[] = [],
): ReservationV4TaskCardResult['fields'][number] {
return createField('/trace_items/0/department_code', {
display_name: 'Department',
display_name: 'Department Code',
value: 'FO',
control_type: 'SELECT',
options_source: 'reservation_v4_trace_department_fixed',

View File

@@ -30,6 +30,15 @@ const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
export type ReservationV4FieldErrorMap = Record<string, string>
export type ReservationV4LookupKind = 'ACCOUNT' | 'ROOM_TYPE' | 'RATE_CODE'
export type ReservationV4FieldValidationErrorType = 'required' | 'positiveInteger'
export type ReservationV4FieldValidationMessageContext = {
type: ReservationV4FieldValidationErrorType
field: ReservationV4TaskCardFieldResult
fieldKey: string
}
export type ReservationV4FieldValidationOptions = {
formatMessage?: (context: ReservationV4FieldValidationMessageContext) => string
}
export function reservationV4FieldKey(field: ReservationV4TaskCardFieldResult): string {
return field.field_pointer || field.field_path
@@ -81,21 +90,42 @@ export function buildReservationV4ReviewOverrides(
export function validateReservationV4Fields(
fields: ReservationV4TaskCardFieldResult[],
values: ReservationRecord,
options: ReservationV4FieldValidationOptions = {},
): ReservationV4FieldErrorMap {
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
const key = reservationV4FieldKey(field)
const value = readV4FieldValue(field, values)
if (field.required && isEmptyV4Value(value)) {
errors[key] = `${field.display_name} is required`
errors[key] = formatValidationMessage('required', field, key, options)
return errors
}
if (isTracePositiveIntegerField(field) && !isEmptyV4Value(value) && !isPositiveIntegerValue(value)) {
errors[key] = `${field.display_name} must be a positive integer`
errors[key] = formatValidationMessage('positiveInteger', field, key, options)
}
return errors
}, {})
}
function formatValidationMessage(
type: ReservationV4FieldValidationErrorType,
field: ReservationV4TaskCardFieldResult,
fieldKey: string,
options: ReservationV4FieldValidationOptions,
): string {
const formattedMessage = options.formatMessage?.({
type,
field,
fieldKey,
})
if (formattedMessage?.trim()) {
return formattedMessage
}
if (type === 'positiveInteger') {
return `${field.display_name} must be a positive integer`
}
return `${field.display_name} is required`
}
export function isReservationV4ConfirmWritableField(field: ReservationV4TaskCardFieldResult): boolean {
if (!isReservationV4EditableField(field)) {
return false

View File

@@ -195,6 +195,7 @@ import type {
ReservationRecord,
ReservationV4OrderTaskDetailResult,
ReservationV4TaskCardResult,
ReservationV4TaskCardFieldResult,
} from '@/types/reservation'
import {
formatReservationTaskCard,
@@ -209,7 +210,9 @@ import {
isReservationV4ReviewWritableField,
isReservationV4TraceSafeField,
mapReservationV4BackendDetailsToFields,
fieldPathToPointer,
reservationV4FieldKey,
type ReservationV4FieldValidationMessageContext,
validateReservationV4Fields,
} from '@/utils/reservationV4FieldRules'
@@ -405,7 +408,11 @@ function canReviewCard(card: ReservationV4TaskCardResult): boolean {
async function submitConfirm(card: ReservationV4TaskCardResult): Promise<void> {
const submissionFields = submissionFieldsForCard(card)
const writableFields = submissionFields.filter(isReservationV4ConfirmWritableField)
const localErrors = validateReservationV4Fields(writableFields, cardValues.value[card.card_id] ?? {})
const localErrors = validateReservationV4Fields(
writableFields,
cardValues.value[card.card_id] ?? {},
{ formatMessage: (context) => formatLocalFieldValidationMessage(card, context) },
)
cardFieldErrors.value = {
...cardFieldErrors.value,
[card.card_id]: localErrors,
@@ -458,7 +465,11 @@ async function submitReview(card: ReservationV4TaskCardResult): Promise<void> {
const submissionFields = submissionFieldsForCard(card)
const writableFields = submissionFields.filter(isReservationV4ReviewWritableField)
const localErrors = validateReservationV4Fields(writableFields, cardValues.value[card.card_id] ?? {})
const localErrors = validateReservationV4Fields(
writableFields,
cardValues.value[card.card_id] ?? {},
{ formatMessage: (context) => formatLocalFieldValidationMessage(card, context) },
)
cardFieldErrors.value = {
...cardFieldErrors.value,
[card.card_id]: localErrors,
@@ -610,6 +621,39 @@ function cardTitle(card: ReservationV4TaskCardResult): string {
return formatReservationTaskCard(t, card.card_type)
}
function formatLocalFieldValidationMessage(
card: ReservationV4TaskCardResult,
context: ReservationV4FieldValidationMessageContext,
): string {
const fieldLabel = localFieldLabel(card, context.field)
if (context.type === 'positiveInteger') {
return t('taskV4.fieldValidation.positiveInteger', { field: fieldLabel })
}
return t('taskV4.fieldValidation.required', { field: fieldLabel })
}
function localFieldLabel(
card: ReservationV4TaskCardResult,
field: ReservationV4TaskCardFieldResult,
): string {
const pointer = field.field_pointer || fieldPathToPointer(field.field_path)
if (isTraceCard(card)) {
if (pointer.endsWith('/text')) {
return t('taskV4.trace.textLabel')
}
if (pointer.endsWith('/department_code')) {
return t('taskV4.trace.department')
}
if (pointer.endsWith('/target_room_type_code')) {
return t('taskV4.trace.targetRoomType')
}
if (pointer.endsWith('/extra_bed_room_count')) {
return t('taskV4.trace.extraBedRoomCount')
}
}
return field.display_name || reservationV4FieldKey(field)
}
function formatApiError(error: unknown, fallback: string): {
message: string
details: unknown