diff --git a/client/src/components/reservation/ReservationV4TraceCard.vue b/client/src/components/reservation/ReservationV4TraceCard.vue
index 3a21522..32a0dba 100644
--- a/client/src/components/reservation/ReservationV4TraceCard.vue
+++ b/client/src/components/reservation/ReservationV4TraceCard.vue
@@ -29,7 +29,7 @@
:class="{ 'trace-field--readonly': !isEditable(field) }"
>
- {{ field.display_name }}
+ {{ traceFieldLabel(field) }}
*
@@ -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}`
diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts
index 9b34771..4d81c00 100644
--- a/client/src/i18n/locales/en-US.ts
+++ b/client/src/i18n/locales/en-US.ts
@@ -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',
diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts
index 5051fde..765fd5a 100644
--- a/client/src/i18n/locales/th-TH.ts
+++ b/client/src/i18n/locales/th-TH.ts
@@ -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: 'กำลังโหลดตัวเลือก',
diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts
index b6e29ff..b4c1d55 100644
--- a/client/src/i18n/locales/zh-CN.ts
+++ b/client/src/i18n/locales/zh-CN.ts
@@ -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: '选项加载中',
diff --git a/client/src/tests/reservationV4Views.spec.ts b/client/src/tests/reservationV4Views.spec.ts
index 129707f..1dbb086 100644
--- a/client/src/tests/reservationV4Views.spec.ts
+++ b/client/src/tests/reservationV4Views.spec.ts
@@ -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',
diff --git a/client/src/utils/reservationV4FieldRules.ts b/client/src/utils/reservationV4FieldRules.ts
index 93d0ebc..a8e4be8 100644
--- a/client/src/utils/reservationV4FieldRules.ts
+++ b/client/src/utils/reservationV4FieldRules.ts
@@ -30,6 +30,15 @@ const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
export type ReservationV4FieldErrorMap = Record
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((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
diff --git a/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue b/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue
index 772d8c5..6fffa13 100644
--- a/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue
+++ b/client/src/views/reservation/ReservationV4OrderTaskDetailView.vue
@@ -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 {
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 {
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