提交代码
This commit is contained in:
@@ -29,7 +29,7 @@
|
|||||||
:class="{ 'trace-field--readonly': !isEditable(field) }"
|
:class="{ 'trace-field--readonly': !isEditable(field) }"
|
||||||
>
|
>
|
||||||
<span class="trace-field__label">
|
<span class="trace-field__label">
|
||||||
{{ field.display_name }}
|
{{ traceFieldLabel(field) }}
|
||||||
<sup v-if="field.required">*</sup>
|
<sup v-if="field.required">*</sup>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -387,6 +387,23 @@ function fieldPointer(field: ReservationV4TaskCardFieldResult): string {
|
|||||||
return field.field_pointer || fieldPathToPointer(field.field_path)
|
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 {
|
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
||||||
if (item.display_name && item.display_name !== item.code) {
|
if (item.display_name && item.display_name !== item.code) {
|
||||||
return `${item.code} - ${item.display_name}`
|
return `${item.code} - ${item.display_name}`
|
||||||
|
|||||||
@@ -628,6 +628,10 @@ export default {
|
|||||||
cardConfirmed: 'Card confirmed and detail refreshed.',
|
cardConfirmed: 'Card confirmed and detail refreshed.',
|
||||||
reviewResolved: 'Review submitted and detail refreshed.',
|
reviewResolved: 'Review submitted and detail refreshed.',
|
||||||
validationFailed: 'Fix current-card field errors first.',
|
validationFailed: 'Fix current-card field errors first.',
|
||||||
|
fieldValidation: {
|
||||||
|
required: '{field} is required',
|
||||||
|
positiveInteger: '{field} must be an integer greater than 0',
|
||||||
|
},
|
||||||
fieldLabels: {
|
fieldLabels: {
|
||||||
basicInformation: {
|
basicInformation: {
|
||||||
accountCode: 'Account Code',
|
accountCode: 'Account Code',
|
||||||
@@ -692,6 +696,10 @@ export default {
|
|||||||
generalTitle: 'General note',
|
generalTitle: 'General note',
|
||||||
extraBedTitle: 'Extra bed note',
|
extraBedTitle: 'Extra bed note',
|
||||||
action: 'Fixed action',
|
action: 'Fixed action',
|
||||||
|
textLabel: 'Follow-up note',
|
||||||
|
department: 'Department',
|
||||||
|
targetRoomType: 'Target room type',
|
||||||
|
extraBedRoomCount: 'Extra bed room count',
|
||||||
},
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: 'Loading options',
|
loading: 'Loading options',
|
||||||
|
|||||||
@@ -628,6 +628,10 @@ export default {
|
|||||||
cardConfirmed: 'ยืนยันการ์ดแล้วและรีเฟรชรายละเอียดแล้ว',
|
cardConfirmed: 'ยืนยันการ์ดแล้วและรีเฟรชรายละเอียดแล้ว',
|
||||||
reviewResolved: 'ส่งผลตรวจสอบแล้วและรีเฟรชรายละเอียดแล้ว',
|
reviewResolved: 'ส่งผลตรวจสอบแล้วและรีเฟรชรายละเอียดแล้ว',
|
||||||
validationFailed: 'โปรดแก้ไขข้อมูลในการ์ดปัจจุบันก่อน',
|
validationFailed: 'โปรดแก้ไขข้อมูลในการ์ดปัจจุบันก่อน',
|
||||||
|
fieldValidation: {
|
||||||
|
required: 'จำเป็นต้องกรอก {field}',
|
||||||
|
positiveInteger: '{field} ต้องเป็นจำนวนเต็มมากกว่า 0',
|
||||||
|
},
|
||||||
fieldLabels: {
|
fieldLabels: {
|
||||||
basicInformation: {
|
basicInformation: {
|
||||||
accountCode: 'รหัสบัญชี',
|
accountCode: 'รหัสบัญชี',
|
||||||
@@ -692,6 +696,10 @@ export default {
|
|||||||
generalTitle: 'หมายเหตุทั่วไป',
|
generalTitle: 'หมายเหตุทั่วไป',
|
||||||
extraBedTitle: 'รายการเตียงเสริม',
|
extraBedTitle: 'รายการเตียงเสริม',
|
||||||
action: 'การดำเนินการคงที่',
|
action: 'การดำเนินการคงที่',
|
||||||
|
textLabel: 'รายละเอียดติดตาม',
|
||||||
|
department: 'แผนกที่รับผิดชอบ',
|
||||||
|
targetRoomType: 'ประเภทห้องเป้าหมาย',
|
||||||
|
extraBedRoomCount: 'จำนวนห้องที่เพิ่มเตียง',
|
||||||
},
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: 'กำลังโหลดตัวเลือก',
|
loading: 'กำลังโหลดตัวเลือก',
|
||||||
|
|||||||
@@ -627,6 +627,10 @@ export default {
|
|||||||
cardConfirmed: '卡片已确认,详情已刷新。',
|
cardConfirmed: '卡片已确认,详情已刷新。',
|
||||||
reviewResolved: '复核已提交,详情已刷新。',
|
reviewResolved: '复核已提交,详情已刷新。',
|
||||||
validationFailed: '请先修正当前卡片字段。',
|
validationFailed: '请先修正当前卡片字段。',
|
||||||
|
fieldValidation: {
|
||||||
|
required: '{field}为必填项',
|
||||||
|
positiveInteger: '{field}必须是大于 0 的整数',
|
||||||
|
},
|
||||||
fieldLabels: {
|
fieldLabels: {
|
||||||
basicInformation: {
|
basicInformation: {
|
||||||
accountCode: '客户代码',
|
accountCode: '客户代码',
|
||||||
@@ -691,6 +695,10 @@ export default {
|
|||||||
generalTitle: '普通事项',
|
generalTitle: '普通事项',
|
||||||
extraBedTitle: '加床事项',
|
extraBedTitle: '加床事项',
|
||||||
action: '固定动作',
|
action: '固定动作',
|
||||||
|
textLabel: '跟进内容',
|
||||||
|
department: '处理部门',
|
||||||
|
targetRoomType: '目标房型',
|
||||||
|
extraBedRoomCount: '加床房间数',
|
||||||
},
|
},
|
||||||
lookup: {
|
lookup: {
|
||||||
loading: '选项加载中',
|
loading: '选项加载中',
|
||||||
|
|||||||
@@ -662,6 +662,10 @@ describe('reservation V4 pages', () => {
|
|||||||
.findAll('.task-card-section')
|
.findAll('.task-card-section')
|
||||||
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
||||||
expect(traceSection).toBeTruthy()
|
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)
|
expect((traceSection!.find('textarea[name="/trace_items/0/text"]').element as HTMLTextAreaElement).value)
|
||||||
.toBe('Late arrival note')
|
.toBe('Late arrival note')
|
||||||
expect(traceSection!.text()).not.toContain('CONTENT_SHOULD_NOT_RENDER')
|
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())
|
.find((section) => section.find('[data-testid="trace-card"]').exists())
|
||||||
expect(traceSection).toBeTruthy()
|
expect(traceSection).toBeTruthy()
|
||||||
expect(traceSection!.text()).toContain('SET EXTRA BED')
|
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')
|
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"]')
|
const roomCountInput = traceSection!.find('input[name="/trace_items/0/extra_bed_room_count"]')
|
||||||
expect(roomCountInput.attributes('type')).toBe('number')
|
expect(roomCountInput.attributes('type')).toBe('number')
|
||||||
@@ -729,7 +739,10 @@ describe('reservation V4 pages', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(service.confirmReservationV4OrderTaskCard).not.toHaveBeenCalled()
|
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 roomCountInput.setValue('2')
|
||||||
await traceSection!.find('button.primary-button').trigger('click')
|
await traceSection!.find('button.primary-button').trigger('click')
|
||||||
@@ -2045,7 +2058,7 @@ function useTraceBusinessCard(
|
|||||||
fields: itemType === 'EXTRA_BED'
|
fields: itemType === 'EXTRA_BED'
|
||||||
? [
|
? [
|
||||||
createField('/trace_items/0/target_room_type_code', {
|
createField('/trace_items/0/target_room_type_code', {
|
||||||
display_name: 'Target Room Type',
|
display_name: 'Target Room Type Code',
|
||||||
value: 'RM2',
|
value: 'RM2',
|
||||||
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
|
options_source: 'RESERVATION_V4_ROOM_TYPE_CATALOG',
|
||||||
control_type: 'SELECT',
|
control_type: 'SELECT',
|
||||||
@@ -2070,7 +2083,7 @@ function useTraceBusinessCard(
|
|||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
createField('/trace_items/0/text', {
|
createField('/trace_items/0/text', {
|
||||||
display_name: 'Trace Text',
|
display_name: 'Trace 内容',
|
||||||
value: 'Late arrival note',
|
value: 'Late arrival note',
|
||||||
control_type: 'TEXTAREA',
|
control_type: 'TEXTAREA',
|
||||||
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
edit_scope: isReviewRequired ? 'REVIEW' : 'CONFIRM',
|
||||||
@@ -2099,7 +2112,7 @@ function createTraceDepartmentField(
|
|||||||
validationErrors: string[] = [],
|
validationErrors: string[] = [],
|
||||||
): ReservationV4TaskCardResult['fields'][number] {
|
): ReservationV4TaskCardResult['fields'][number] {
|
||||||
return createField('/trace_items/0/department_code', {
|
return createField('/trace_items/0/department_code', {
|
||||||
display_name: 'Department',
|
display_name: 'Department Code',
|
||||||
value: 'FO',
|
value: 'FO',
|
||||||
control_type: 'SELECT',
|
control_type: 'SELECT',
|
||||||
options_source: 'reservation_v4_trace_department_fixed',
|
options_source: 'reservation_v4_trace_department_fixed',
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ const hiddenDisplayValue = Symbol('reservation-v4-hidden-display-value')
|
|||||||
|
|
||||||
export type ReservationV4FieldErrorMap = Record<string, string>
|
export type ReservationV4FieldErrorMap = Record<string, string>
|
||||||
export type ReservationV4LookupKind = 'ACCOUNT' | 'ROOM_TYPE' | 'RATE_CODE'
|
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 {
|
export function reservationV4FieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||||
return field.field_pointer || field.field_path
|
return field.field_pointer || field.field_path
|
||||||
@@ -81,21 +90,42 @@ export function buildReservationV4ReviewOverrides(
|
|||||||
export function validateReservationV4Fields(
|
export function validateReservationV4Fields(
|
||||||
fields: ReservationV4TaskCardFieldResult[],
|
fields: ReservationV4TaskCardFieldResult[],
|
||||||
values: ReservationRecord,
|
values: ReservationRecord,
|
||||||
|
options: ReservationV4FieldValidationOptions = {},
|
||||||
): ReservationV4FieldErrorMap {
|
): ReservationV4FieldErrorMap {
|
||||||
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
|
return fields.filter(isReservationV4EditableField).reduce<ReservationV4FieldErrorMap>((errors, field) => {
|
||||||
const key = reservationV4FieldKey(field)
|
const key = reservationV4FieldKey(field)
|
||||||
const value = readV4FieldValue(field, values)
|
const value = readV4FieldValue(field, values)
|
||||||
if (field.required && isEmptyV4Value(value)) {
|
if (field.required && isEmptyV4Value(value)) {
|
||||||
errors[key] = `${field.display_name} is required`
|
errors[key] = formatValidationMessage('required', field, key, options)
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
if (isTracePositiveIntegerField(field) && !isEmptyV4Value(value) && !isPositiveIntegerValue(value)) {
|
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
|
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 {
|
export function isReservationV4ConfirmWritableField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||||
if (!isReservationV4EditableField(field)) {
|
if (!isReservationV4EditableField(field)) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ import type {
|
|||||||
ReservationRecord,
|
ReservationRecord,
|
||||||
ReservationV4OrderTaskDetailResult,
|
ReservationV4OrderTaskDetailResult,
|
||||||
ReservationV4TaskCardResult,
|
ReservationV4TaskCardResult,
|
||||||
|
ReservationV4TaskCardFieldResult,
|
||||||
} from '@/types/reservation'
|
} from '@/types/reservation'
|
||||||
import {
|
import {
|
||||||
formatReservationTaskCard,
|
formatReservationTaskCard,
|
||||||
@@ -209,7 +210,9 @@ import {
|
|||||||
isReservationV4ReviewWritableField,
|
isReservationV4ReviewWritableField,
|
||||||
isReservationV4TraceSafeField,
|
isReservationV4TraceSafeField,
|
||||||
mapReservationV4BackendDetailsToFields,
|
mapReservationV4BackendDetailsToFields,
|
||||||
|
fieldPathToPointer,
|
||||||
reservationV4FieldKey,
|
reservationV4FieldKey,
|
||||||
|
type ReservationV4FieldValidationMessageContext,
|
||||||
validateReservationV4Fields,
|
validateReservationV4Fields,
|
||||||
} from '@/utils/reservationV4FieldRules'
|
} from '@/utils/reservationV4FieldRules'
|
||||||
|
|
||||||
@@ -405,7 +408,11 @@ function canReviewCard(card: ReservationV4TaskCardResult): boolean {
|
|||||||
async function submitConfirm(card: ReservationV4TaskCardResult): Promise<void> {
|
async function submitConfirm(card: ReservationV4TaskCardResult): Promise<void> {
|
||||||
const submissionFields = submissionFieldsForCard(card)
|
const submissionFields = submissionFieldsForCard(card)
|
||||||
const writableFields = submissionFields.filter(isReservationV4ConfirmWritableField)
|
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 = {
|
||||||
...cardFieldErrors.value,
|
...cardFieldErrors.value,
|
||||||
[card.card_id]: localErrors,
|
[card.card_id]: localErrors,
|
||||||
@@ -458,7 +465,11 @@ async function submitReview(card: ReservationV4TaskCardResult): Promise<void> {
|
|||||||
|
|
||||||
const submissionFields = submissionFieldsForCard(card)
|
const submissionFields = submissionFieldsForCard(card)
|
||||||
const writableFields = submissionFields.filter(isReservationV4ReviewWritableField)
|
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 = {
|
||||||
...cardFieldErrors.value,
|
...cardFieldErrors.value,
|
||||||
[card.card_id]: localErrors,
|
[card.card_id]: localErrors,
|
||||||
@@ -610,6 +621,39 @@ function cardTitle(card: ReservationV4TaskCardResult): string {
|
|||||||
return formatReservationTaskCard(t, card.card_type)
|
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): {
|
function formatApiError(error: unknown, fallback: string): {
|
||||||
message: string
|
message: string
|
||||||
details: unknown
|
details: unknown
|
||||||
|
|||||||
Reference in New Issue
Block a user