diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 4bff61b..6676f0c 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -315,6 +315,13 @@ export default { noPdfUrl: 'The backend did not return a PDF URL.', backendTotals: 'Backend totals', validationTitle: 'Please fix the form', + fieldErrors: { + required: 'Required', + invalidEmail: 'Invalid email', + dateOrder: 'Invalid date order', + positiveNumber: 'Must be greater than 0', + nonNegativeNumber: 'Cannot be less than 0', + }, statuses: { SUCCEEDED: 'Generated', GENERATED: 'Generated', diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index 74d2a4f..6bc048b 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -315,6 +315,13 @@ export default { noPdfUrl: 'หลังบ้านไม่ได้ส่งลิงก์ PDF กลับมา', backendTotals: 'ยอดจากหลังบ้าน', validationTitle: 'กรุณาแก้ไขแบบฟอร์ม', + fieldErrors: { + required: 'จำเป็นต้องกรอก', + invalidEmail: 'รูปแบบ Email ไม่ถูกต้อง', + dateOrder: 'ลำดับวันที่ไม่ถูกต้อง', + positiveNumber: 'ต้องมากกว่า 0', + nonNegativeNumber: 'ต้องไม่ต่ำกว่า 0', + }, statuses: { SUCCEEDED: 'สร้างแล้ว', GENERATED: 'สร้างแล้ว', diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index bac296f..a0fc2f7 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -315,6 +315,13 @@ export default { noPdfUrl: '后端未返回 PDF 链接。', backendTotals: '后端返回金额', validationTitle: '请先修正表单', + fieldErrors: { + required: '必填', + invalidEmail: 'Email 格式不正确', + dateOrder: '日期顺序不正确', + positiveNumber: '必须大于 0', + nonNegativeNumber: '不能小于 0', + }, statuses: { SUCCEEDED: '已生成', GENERATED: '已生成', diff --git a/client/src/tests/manualInvoiceView.spec.ts b/client/src/tests/manualInvoiceView.spec.ts index 4808d78..68160ce 100644 --- a/client/src/tests/manualInvoiceView.spec.ts +++ b/client/src/tests/manualInvoiceView.spec.ts @@ -270,6 +270,62 @@ describe('ReservationManualInvoiceView', () => { expect(wrapper.text()).not.toContain('例如 includingBF') }) + it('marks required fields and highlights empty inputs after submit', async () => { + const wrapper = mountView() + + expect(wrapper.find('label[for="invoice-company"].field-label--required').exists()).toBe(true) + expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).not.toContain('is-invalid') + + await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click') + await flushPromises() + + expect(service.generateManualReservationInvoice).not.toHaveBeenCalled() + expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="recipient-company-input"]').attributes('aria-invalid')).toBe('true') + expect(wrapper.find('[data-testid="manual-invoice-field-error-company"]').text()).toBe( + zhCN.manualInvoice.fieldErrors.required, + ) + expect(wrapper.find('[data-testid="booking-group-name"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="primary-room-count"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="charge-description-0"]').classes()).toContain('is-invalid') + + await wrapper.find('[data-testid="recipient-company-input"]').setValue('Manual Company') + + expect(wrapper.find('[data-testid="recipient-company-input"]').classes()).not.toContain('is-invalid') + expect(wrapper.find('[data-testid="manual-invoice-field-error-company"]').exists()).toBe(false) + }) + + it('shows field-level validation messages for invalid email, dates, and numbers', async () => { + const wrapper = mountView() + + await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD') + await fillMinimumInvoiceForm(wrapper) + await wrapper.find('[data-testid="recipient-email"]').setValue('bad-email') + await wrapper.find('[data-testid="booking-departure-date"]').setValue('2026-07-19') + await wrapper.find('[data-testid="booking-extra-bed-rate"]').setValue('-1') + await wrapper.find('[data-testid="charge-rate-0"]').setValue('0') + await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click') + await flushPromises() + + expect(service.generateManualReservationInvoice).not.toHaveBeenCalled() + expect(wrapper.find('[data-testid="recipient-email"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="manual-invoice-field-error-email"]').text()).toBe( + zhCN.manualInvoice.fieldErrors.invalidEmail, + ) + expect(wrapper.find('[data-testid="booking-departure-date"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="manual-invoice-field-error-departure-date"]').text()).toBe( + zhCN.manualInvoice.fieldErrors.dateOrder, + ) + expect(wrapper.find('[data-testid="booking-extra-bed-rate"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="manual-invoice-field-error-extra-bed-rate"]').text()).toBe( + zhCN.manualInvoice.fieldErrors.nonNegativeNumber, + ) + expect(wrapper.find('[data-testid="charge-rate-0"]').classes()).toContain('is-invalid') + expect(wrapper.find('[data-testid="manual-invoice-field-error-charge-0-rate"]').text()).toBe( + zhCN.manualInvoice.fieldErrors.positiveNumber, + ) + }) + it('shows final-format date previews like the invoice prototype', async () => { const wrapper = mountView() diff --git a/client/src/views/reservation/ReservationManualInvoiceView.vue b/client/src/views/reservation/ReservationManualInvoiceView.vue index 9ea1e16..36db2bd 100644 --- a/client/src/views/reservation/ReservationManualInvoiceView.vue +++ b/client/src/views/reservation/ReservationManualInvoiceView.vue @@ -42,7 +42,10 @@
- +
+

+ {{ fieldError('attention') }} +