From 7e2cd8969f67ad4752384e93ffa56b52924f96a5 Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 22 Jul 2026 15:30:26 +0700 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationV4TraceCard.vue | 19 +++++++- client/src/i18n/locales/en-US.ts | 8 ++++ client/src/i18n/locales/th-TH.ts | 8 ++++ client/src/i18n/locales/zh-CN.ts | 8 ++++ client/src/tests/reservationV4Views.spec.ts | 21 ++++++-- client/src/utils/reservationV4FieldRules.ts | 34 ++++++++++++- .../ReservationV4OrderTaskDetailView.vue | 48 ++++++++++++++++++- 7 files changed, 137 insertions(+), 9 deletions(-) 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