完善手工开票页面文案国际化
This commit is contained in:
@@ -253,6 +253,7 @@ export default {
|
|||||||
},
|
},
|
||||||
manualInvoice: {
|
manualInvoice: {
|
||||||
title: 'Manual invoice',
|
title: 'Manual invoice',
|
||||||
|
createTitle: 'Create Invoice',
|
||||||
subtitle: 'Generate a Proforma Invoice PDF by entering fields manually without an order or task.',
|
subtitle: 'Generate a Proforma Invoice PDF by entering fields manually without an order or task.',
|
||||||
sourceBadge: 'Manual generation',
|
sourceBadge: 'Manual generation',
|
||||||
independentHint: 'This V1 page is not linked to an order or task. The backend records generation and audit data.',
|
independentHint: 'This V1 page is not linked to an order or task. The backend records generation and audit data.',
|
||||||
@@ -272,12 +273,16 @@ export default {
|
|||||||
company: 'Company',
|
company: 'Company',
|
||||||
attention: 'Attention',
|
attention: 'Attention',
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
companyInputPlaceholder: 'Enter company',
|
||||||
|
attentionInputPlaceholder: 'Enter attention',
|
||||||
|
addressInputPlaceholder: 'Enter full billing address',
|
||||||
telephone: 'Tel',
|
telephone: 'Tel',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
groupName: 'Group name',
|
groupName: 'Group name',
|
||||||
arrivalDate: 'Arrival date',
|
arrivalDate: 'Arrival date',
|
||||||
departureDate: 'Departure date',
|
departureDate: 'Departure date',
|
||||||
roomRateNote: 'Room rate note',
|
roomRateNote: 'Room rate note',
|
||||||
|
roomRateNotePlaceholder: 'e.g. including BF',
|
||||||
extraBedRate: 'Extra bed rate',
|
extraBedRate: 'Extra bed rate',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
roomType: 'Room type',
|
roomType: 'Room type',
|
||||||
@@ -288,12 +293,13 @@ export default {
|
|||||||
subtotal: 'Sub-total',
|
subtotal: 'Sub-total',
|
||||||
vat: 'VAT 7%',
|
vat: 'VAT 7%',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
addCharge: 'Add charge',
|
addCharge: 'Add row',
|
||||||
removeCharge: 'Remove',
|
removeCharge: 'Remove',
|
||||||
|
removeChargeLineAria: 'Remove row {index}',
|
||||||
maxChargesReached: 'V1 supports up to 10 charge lines.',
|
maxChargesReached: 'V1 supports up to 10 charge lines.',
|
||||||
previewDisclaimer: 'The frontend only previews amounts. Backend totals are authoritative.',
|
previewDisclaimer: 'The frontend only previews amounts. Backend totals are authoritative.',
|
||||||
manualOverrideHint: 'Selecting company and attention fills contact details; every text field remains editable.',
|
manualOverrideHint: 'Selecting company and attention fills contact details; every text field remains editable.',
|
||||||
generate: 'Generate PDF',
|
generate: 'Confirm and generate PDF',
|
||||||
generating: 'Generating',
|
generating: 'Generating',
|
||||||
openPdf: 'Open PDF',
|
openPdf: 'Open PDF',
|
||||||
downloadPdf: 'Download PDF',
|
downloadPdf: 'Download PDF',
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export default {
|
|||||||
},
|
},
|
||||||
manualInvoice: {
|
manualInvoice: {
|
||||||
title: 'ออกใบแจ้งหนี้เอง',
|
title: 'ออกใบแจ้งหนี้เอง',
|
||||||
|
createTitle: 'สร้าง Invoice',
|
||||||
subtitle: 'กรอกข้อมูลเองเพื่อสร้าง Proforma Invoice PDF โดยไม่ต้องอ้างอิงออเดอร์หรืองาน',
|
subtitle: 'กรอกข้อมูลเองเพื่อสร้าง Proforma Invoice PDF โดยไม่ต้องอ้างอิงออเดอร์หรืองาน',
|
||||||
sourceBadge: 'สร้างด้วยตนเอง',
|
sourceBadge: 'สร้างด้วยตนเอง',
|
||||||
independentHint: 'หน้า V1 นี้ไม่ผูกกับออเดอร์หรืองาน หลังบ้านจะบันทึกประวัติการสร้างและ audit',
|
independentHint: 'หน้า V1 นี้ไม่ผูกกับออเดอร์หรืองาน หลังบ้านจะบันทึกประวัติการสร้างและ audit',
|
||||||
@@ -272,12 +273,16 @@ export default {
|
|||||||
company: 'Company',
|
company: 'Company',
|
||||||
attention: 'Attention',
|
attention: 'Attention',
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
|
companyInputPlaceholder: 'กรอกชื่อบริษัท',
|
||||||
|
attentionInputPlaceholder: 'กรอกชื่อผู้รับ',
|
||||||
|
addressInputPlaceholder: 'กรอกที่อยู่เรียกเก็บเงินแบบเต็ม',
|
||||||
telephone: 'Tel',
|
telephone: 'Tel',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
groupName: 'Group Name',
|
groupName: 'Group Name',
|
||||||
arrivalDate: 'Arrival Date',
|
arrivalDate: 'Arrival Date',
|
||||||
departureDate: 'Departure Date',
|
departureDate: 'Departure Date',
|
||||||
roomRateNote: 'Room Rate Note',
|
roomRateNote: 'Room Rate Note',
|
||||||
|
roomRateNotePlaceholder: 'เช่น including BF',
|
||||||
extraBedRate: 'Extra Bed Rate',
|
extraBedRate: 'Extra Bed Rate',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
roomType: 'Room Type',
|
roomType: 'Room Type',
|
||||||
@@ -288,12 +293,13 @@ export default {
|
|||||||
subtotal: 'Sub-Total',
|
subtotal: 'Sub-Total',
|
||||||
vat: 'VAT 7%',
|
vat: 'VAT 7%',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
addCharge: 'เพิ่มรายการ',
|
addCharge: 'เพิ่มแถว',
|
||||||
removeCharge: 'ลบ',
|
removeCharge: 'ลบ',
|
||||||
|
removeChargeLineAria: 'ลบแถวที่ {index}',
|
||||||
maxChargesReached: 'V1 รองรับได้สูงสุด 10 รายการค่าใช้จ่าย',
|
maxChargesReached: 'V1 รองรับได้สูงสุด 10 รายการค่าใช้จ่าย',
|
||||||
previewDisclaimer: 'ยอดนี้เป็นเพียงตัวอย่าง ยอดสุดท้ายให้ยึด totals จากหลังบ้าน',
|
previewDisclaimer: 'ยอดนี้เป็นเพียงตัวอย่าง ยอดสุดท้ายให้ยึด totals จากหลังบ้าน',
|
||||||
manualOverrideHint: 'เลือก Company / Attention แล้วระบบจะกรอกข้อมูลให้ แต่ยังแก้ไขข้อความได้ทั้งหมด',
|
manualOverrideHint: 'เลือก Company / Attention แล้วระบบจะกรอกข้อมูลให้ แต่ยังแก้ไขข้อความได้ทั้งหมด',
|
||||||
generate: 'สร้าง PDF',
|
generate: 'ยืนยันและสร้าง PDF',
|
||||||
generating: 'กำลังสร้าง',
|
generating: 'กำลังสร้าง',
|
||||||
openPdf: 'เปิด PDF',
|
openPdf: 'เปิด PDF',
|
||||||
downloadPdf: 'ดาวน์โหลด PDF',
|
downloadPdf: 'ดาวน์โหลด PDF',
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export default {
|
|||||||
},
|
},
|
||||||
manualInvoice: {
|
manualInvoice: {
|
||||||
title: '手工开票',
|
title: '手工开票',
|
||||||
|
createTitle: '创建 Invoice',
|
||||||
subtitle: '无订单、无任务时,手工填写 Proforma Invoice 字段并生成 PDF。',
|
subtitle: '无订单、无任务时,手工填写 Proforma Invoice 字段并生成 PDF。',
|
||||||
sourceBadge: '手工生成',
|
sourceBadge: '手工生成',
|
||||||
independentHint: '本页第一版不关联订单或任务,生成记录和审计由后端统一落库。',
|
independentHint: '本页第一版不关联订单或任务,生成记录和审计由后端统一落库。',
|
||||||
@@ -272,12 +273,16 @@ export default {
|
|||||||
company: '公司(Company)',
|
company: '公司(Company)',
|
||||||
attention: '收件人(Attention)',
|
attention: '收件人(Attention)',
|
||||||
address: '地址(Address)',
|
address: '地址(Address)',
|
||||||
|
companyInputPlaceholder: '输入公司名称',
|
||||||
|
attentionInputPlaceholder: '输入收件人',
|
||||||
|
addressInputPlaceholder: '输入客户完整账单地址',
|
||||||
telephone: 'Tel',
|
telephone: 'Tel',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
groupName: '团队名称(Group Name)',
|
groupName: '团队名称(Group Name)',
|
||||||
arrivalDate: '到店日期(Arrival Date)',
|
arrivalDate: '到店日期(Arrival Date)',
|
||||||
departureDate: '离店日期(Departure Date)',
|
departureDate: '离店日期(Departure Date)',
|
||||||
roomRateNote: '房价备注(Room Rate Note)',
|
roomRateNote: '房价备注(Room Rate Note)',
|
||||||
|
roomRateNotePlaceholder: '例如 including BF',
|
||||||
extraBedRate: '加床价格(Extra Bed Rate)',
|
extraBedRate: '加床价格(Extra Bed Rate)',
|
||||||
description: '费用描述(Description)',
|
description: '费用描述(Description)',
|
||||||
roomType: '房型(Room Type)',
|
roomType: '房型(Room Type)',
|
||||||
@@ -288,12 +293,13 @@ export default {
|
|||||||
subtotal: 'Sub-Total',
|
subtotal: 'Sub-Total',
|
||||||
vat: 'VAT 7%',
|
vat: 'VAT 7%',
|
||||||
total: 'Total',
|
total: 'Total',
|
||||||
addCharge: '新增费用',
|
addCharge: '新增一行',
|
||||||
removeCharge: '删除',
|
removeCharge: '删除',
|
||||||
|
removeChargeLineAria: '删除第 {index} 行',
|
||||||
maxChargesReached: '第一版最多支持 10 条费用明细。',
|
maxChargesReached: '第一版最多支持 10 条费用明细。',
|
||||||
previewDisclaimer: '前端仅做金额预览,最终金额以后端返回 totals 为准。',
|
previewDisclaimer: '前端仅做金额预览,最终金额以后端返回 totals 为准。',
|
||||||
manualOverrideHint: '选择 Company / Attention 后会自动带出资料,带出的文本仍可手工覆盖。',
|
manualOverrideHint: '选择 Company / Attention 后会自动带出资料,带出的文本仍可手工覆盖。',
|
||||||
generate: '生成 PDF',
|
generate: '确认并生成 PDF',
|
||||||
generating: '生成中',
|
generating: '生成中',
|
||||||
openPdf: '打开 PDF',
|
openPdf: '打开 PDF',
|
||||||
downloadPdf: '下载 PDF',
|
downloadPdf: '下载 PDF',
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import enUS from '@/i18n/locales/en-US'
|
||||||
|
import thTH from '@/i18n/locales/th-TH'
|
||||||
import zhCN from '@/i18n/locales/zh-CN'
|
import zhCN from '@/i18n/locales/zh-CN'
|
||||||
import { ApiError } from '@/services/httpClient'
|
import { ApiError } from '@/services/httpClient'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
@@ -46,12 +48,17 @@ function createResult(): ManualInvoiceGenerationResult {
|
|||||||
const nativeCreateObjectURL = URL.createObjectURL
|
const nativeCreateObjectURL = URL.createObjectURL
|
||||||
const nativeRevokeObjectURL = URL.revokeObjectURL
|
const nativeRevokeObjectURL = URL.revokeObjectURL
|
||||||
|
|
||||||
function mountView(options: { timeZone?: string; extraHotels?: AuthHotelResult[] } = {}) {
|
type ManualInvoiceTestLocale = 'zh-CN' | 'en-US' | 'th-TH'
|
||||||
|
|
||||||
|
function mountView(options: { timeZone?: string; extraHotels?: AuthHotelResult[]; locale?: ManualInvoiceTestLocale } = {}) {
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: 'zh-CN',
|
locale: options.locale ?? 'zh-CN',
|
||||||
|
fallbackLocale: 'zh-CN',
|
||||||
messages: {
|
messages: {
|
||||||
'zh-CN': zhCN,
|
'zh-CN': zhCN,
|
||||||
|
'en-US': enUS,
|
||||||
|
'th-TH': thTH,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@@ -80,11 +87,17 @@ function mountView(options: { timeZone?: string; extraHotels?: AuthHotelResult[]
|
|||||||
menus: [],
|
menus: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
return mount(ReservationManualInvoiceView, {
|
const wrapper = mount(ReservationManualInvoiceView, {
|
||||||
global: {
|
global: {
|
||||||
plugins: [i18n, pinia],
|
plugins: [i18n, pinia],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return Object.assign(wrapper, {
|
||||||
|
async setLocale(locale: ManualInvoiceTestLocale) {
|
||||||
|
i18n.global.locale.value = locale
|
||||||
|
await nextTick()
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fillMinimumInvoiceForm(wrapper: ReturnType<typeof mountView>) {
|
async function fillMinimumInvoiceForm(wrapper: ReturnType<typeof mountView>) {
|
||||||
@@ -214,15 +227,49 @@ describe('ReservationManualInvoiceView', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('[data-testid="manual-invoice-prototype-page"]').exists()).toBe(true)
|
expect(wrapper.find('[data-testid="manual-invoice-prototype-page"]').exists()).toBe(true)
|
||||||
expect(wrapper.text()).toContain('创建 Invoice')
|
expect(wrapper.text()).toContain('创建 Invoice')
|
||||||
|
expect(wrapper.text()).not.toContain('草稿')
|
||||||
expect(wrapper.text()).toContain('01')
|
expect(wrapper.text()).toContain('01')
|
||||||
expect(wrapper.text()).toContain('Basic Information')
|
expect(wrapper.text()).toContain('Basic Information')
|
||||||
expect(wrapper.text()).toContain('02')
|
expect(wrapper.text()).toContain('02')
|
||||||
expect(wrapper.text()).toContain('Please refer to the following reservation')
|
expect(wrapper.text()).toContain('Please refer to the following reservation')
|
||||||
|
expect(wrapper.text()).not.toContain('Group Name 与 Group Code 在本流程中为同一字段。')
|
||||||
expect(wrapper.text()).toContain('03')
|
expect(wrapper.text()).toContain('03')
|
||||||
expect(wrapper.text()).toContain('Description&Amount')
|
expect(wrapper.text()).toContain('Description&Amount')
|
||||||
expect(wrapper.find('[data-testid="manual-invoice-action-bar"]').exists()).toBe(true)
|
expect(wrapper.find('[data-testid="manual-invoice-action-bar"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('localizes the highlighted invoice title and recipient placeholders', () => {
|
||||||
|
const wrapper = mountView({ locale: 'en-US' })
|
||||||
|
const placeholders = wrapper.findAll('input, textarea').map((field) => field.attributes('placeholder'))
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Create Invoice')
|
||||||
|
expect(wrapper.text()).not.toContain('创建 Invoice')
|
||||||
|
expect(placeholders).toContain('Enter company')
|
||||||
|
expect(placeholders).toContain('Enter attention')
|
||||||
|
expect(placeholders).toContain('Enter full billing address')
|
||||||
|
expect(placeholders).not.toContain('输入 Company')
|
||||||
|
expect(placeholders).not.toContain('输入 Attention')
|
||||||
|
expect(placeholders).not.toContain('输入客户完整账单地址')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('localizes the invoice action controls and room rate note placeholder', async () => {
|
||||||
|
const wrapper = mountView({ locale: 'en-US' })
|
||||||
|
|
||||||
|
expect((wrapper.find('[data-testid="booking-room-rate-note"]').element as HTMLInputElement).placeholder).toBe(
|
||||||
|
'e.g. including BF',
|
||||||
|
)
|
||||||
|
expect(wrapper.find('[data-testid="manual-invoice-add-charge"]').text()).toContain('Add row')
|
||||||
|
expect(wrapper.find('[data-testid="manual-invoice-submit"]').text()).toContain('Confirm and generate PDF')
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="manual-invoice-add-charge"]').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.findAll('.remove-button')[1]?.text()).toContain('Remove')
|
||||||
|
expect(wrapper.findAll('.remove-button')[1]?.attributes('aria-label')).toBe('Remove row 2')
|
||||||
|
expect(wrapper.text()).not.toContain('新增一行')
|
||||||
|
expect(wrapper.text()).not.toContain('确认并生成 PDF')
|
||||||
|
expect(wrapper.text()).not.toContain('例如 includingBF')
|
||||||
|
})
|
||||||
|
|
||||||
it('shows final-format date previews like the invoice prototype', async () => {
|
it('shows final-format date previews like the invoice prototype', async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
|
|
||||||
@@ -287,8 +334,8 @@ describe('ReservationManualInvoiceView', () => {
|
|||||||
await wrapper.find('[data-testid="recipient-company-code"]').setValue('MANUAL')
|
await wrapper.find('[data-testid="recipient-company-code"]').setValue('MANUAL')
|
||||||
|
|
||||||
expect((wrapper.find('[data-testid="recipient-contact-id"]').element as HTMLSelectElement).value).toBe('MANUAL')
|
expect((wrapper.find('[data-testid="recipient-contact-id"]').element as HTMLSelectElement).value).toBe('MANUAL')
|
||||||
expect((wrapper.find('input[placeholder="输入 Company"]').element as HTMLInputElement).value).toBe('')
|
expect((wrapper.find('[data-testid="recipient-company-input"]').element as HTMLInputElement).value).toBe('')
|
||||||
expect((wrapper.find('input[placeholder="输入 Attention"]').element as HTMLInputElement).value).toBe('')
|
expect((wrapper.find('[data-testid="recipient-attention-input"]').element as HTMLInputElement).value).toBe('')
|
||||||
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe('')
|
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -370,6 +417,32 @@ describe('ReservationManualInvoiceView', () => {
|
|||||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:manual-invoice-pdf')
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:manual-invoice-pdf')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('localizes the PDF download fallback message after a locale switch', async () => {
|
||||||
|
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
||||||
|
const fetchMock = vi.fn().mockRejectedValue(new Error('CORS blocked'))
|
||||||
|
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
const wrapper = mountView()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD')
|
||||||
|
await fillMinimumInvoiceForm(wrapper)
|
||||||
|
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
await wrapper.find('[data-testid="manual-invoice-download-pdf"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(openSpy).toHaveBeenCalledWith('https://oss.example/invoices/manual-91001.pdf', '_blank', 'noopener,noreferrer')
|
||||||
|
expect(wrapper.find('[data-testid="manual-invoice-download-message"]').text()).toBe(
|
||||||
|
zhCN.manualInvoice.downloadFallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
await wrapper.setLocale('en-US')
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="manual-invoice-download-message"]').text()).toBe(
|
||||||
|
enUS.manualInvoice.downloadFallback,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('submits complete numeric input values instead of parseFloat prefixes', async () => {
|
it('submits complete numeric input values instead of parseFloat prefixes', async () => {
|
||||||
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
|
|||||||
@@ -5,14 +5,7 @@
|
|||||||
>
|
>
|
||||||
<header class="invoice-page__header">
|
<header class="invoice-page__header">
|
||||||
<div class="invoice-page__title">
|
<div class="invoice-page__title">
|
||||||
<h1>创建 Invoice</h1>
|
<h1>{{ t('manualInvoice.createTitle') }}</h1>
|
||||||
<span class="draft-badge">
|
|
||||||
<i
|
|
||||||
class="pi pi-pencil"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
草稿
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="source-summary">
|
<div class="source-summary">
|
||||||
<span>当前来源</span>
|
<span>当前来源</span>
|
||||||
@@ -70,9 +63,10 @@
|
|||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
v-model.trim="form.recipient.company"
|
v-model.trim="form.recipient.company"
|
||||||
|
data-testid="recipient-company-input"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="输入 Company"
|
:placeholder="t('manualInvoice.companyInputPlaceholder')"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,9 +95,10 @@
|
|||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
v-model.trim="form.recipient.attention"
|
v-model.trim="form.recipient.attention"
|
||||||
|
data-testid="recipient-attention-input"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="输入 Attention"
|
:placeholder="t('manualInvoice.attentionInputPlaceholder')"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,8 +108,9 @@
|
|||||||
<span class="field-label">Address</span>
|
<span class="field-label">Address</span>
|
||||||
<textarea
|
<textarea
|
||||||
v-model.trim="form.recipient.address"
|
v-model.trim="form.recipient.address"
|
||||||
|
data-testid="recipient-address"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="输入客户完整账单地址"
|
:placeholder="t('manualInvoice.addressInputPlaceholder')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -189,7 +185,6 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<small class="field__hint">Group Name 与 Group Code 在本流程中为同一字段。</small>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -290,7 +285,7 @@
|
|||||||
v-model.trim="form.booking.room_rate_note"
|
v-model.trim="form.booking.room_rate_note"
|
||||||
data-testid="booking-room-rate-note"
|
data-testid="booking-room-rate-note"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="例如 includingBF"
|
:placeholder="t('manualInvoice.roomRateNotePlaceholder')"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
@@ -377,7 +372,7 @@
|
|||||||
class="pi pi-plus"
|
class="pi pi-plus"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
新增一行
|
{{ t('manualInvoice.addCharge') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -395,14 +390,14 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="remove-button"
|
class="remove-button"
|
||||||
:disabled="form.charges.length === 1"
|
:disabled="form.charges.length === 1"
|
||||||
:aria-label="`删除第 ${index + 1} 行`"
|
:aria-label="t('manualInvoice.removeChargeLineAria', { index: index + 1 })"
|
||||||
@click="removeCharge(index)"
|
@click="removeCharge(index)"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="pi pi-trash"
|
class="pi pi-trash"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
删除
|
{{ t('manualInvoice.removeCharge') }}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -504,7 +499,7 @@
|
|||||||
class="pi pi-file-pdf"
|
class="pi pi-file-pdf"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{{ submitting ? '生成中' : '确认并生成 PDF' }}
|
{{ submitting ? t('manualInvoice.generating') : t('manualInvoice.generate') }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -773,7 +768,7 @@ const submitting = ref(false)
|
|||||||
const downloadingPdf = ref(false)
|
const downloadingPdf = ref(false)
|
||||||
const messages = ref<string[]>([])
|
const messages = ref<string[]>([])
|
||||||
const technicalDetails = ref<string[]>([])
|
const technicalDetails = ref<string[]>([])
|
||||||
const downloadMessage = ref('')
|
const downloadMessageKey = ref('')
|
||||||
const alertTone = ref<'error' | 'success'>('error')
|
const alertTone = ref<'error' | 'success'>('error')
|
||||||
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
|
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
|
||||||
const documentDatesEdited = ref(false)
|
const documentDatesEdited = ref(false)
|
||||||
@@ -785,6 +780,7 @@ const recipientContacts = computed(() => {
|
|||||||
const primaryCharge = computed<ChargeForm>(() => form.charges[0] as ChargeForm)
|
const primaryCharge = computed<ChargeForm>(() => form.charges[0] as ChargeForm)
|
||||||
|
|
||||||
const sourceGroupName = computed(() => form.booking.group_name.trim() || '未填写')
|
const sourceGroupName = computed(() => form.booking.group_name.trim() || '未填写')
|
||||||
|
const downloadMessage = computed(() => (downloadMessageKey.value ? t(downloadMessageKey.value) : ''))
|
||||||
|
|
||||||
const previewTotals = computed(() => {
|
const previewTotals = computed(() => {
|
||||||
const total = form.charges.reduce((sum, charge) => sum + lineAmount(charge), 0)
|
const total = form.charges.reduce((sum, charge) => sum + lineAmount(charge), 0)
|
||||||
@@ -884,7 +880,7 @@ function lineAmount(charge: ChargeForm): number {
|
|||||||
|
|
||||||
async function submitInvoice(): Promise<void> {
|
async function submitInvoice(): Promise<void> {
|
||||||
technicalDetails.value = []
|
technicalDetails.value = []
|
||||||
downloadMessage.value = ''
|
downloadMessageKey.value = ''
|
||||||
messages.value = validateForm()
|
messages.value = validateForm()
|
||||||
alertTone.value = 'error'
|
alertTone.value = 'error'
|
||||||
if (messages.value.length > 0) {
|
if (messages.value.length > 0) {
|
||||||
@@ -913,7 +909,7 @@ async function downloadPdf(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadingPdf.value = true
|
downloadingPdf.value = true
|
||||||
downloadMessage.value = ''
|
downloadMessageKey.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await fetch(pdfUrl, { credentials: 'omit' })
|
const response = await fetch(pdfUrl, { credentials: 'omit' })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -924,7 +920,7 @@ async function downloadPdf(): Promise<void> {
|
|||||||
triggerBrowserDownload(objectUrl, createPdfFileName(result.invoice_generation_id))
|
triggerBrowserDownload(objectUrl, createPdfFileName(result.invoice_generation_id))
|
||||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
|
||||||
} catch {
|
} catch {
|
||||||
downloadMessage.value = t('manualInvoice.downloadFallback')
|
downloadMessageKey.value = 'manualInvoice.downloadFallback'
|
||||||
window.open(pdfUrl, '_blank', 'noopener,noreferrer')
|
window.open(pdfUrl, '_blank', 'noopener,noreferrer')
|
||||||
} finally {
|
} finally {
|
||||||
downloadingPdf.value = false
|
downloadingPdf.value = false
|
||||||
@@ -1253,19 +1249,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draft-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fff5df;
|
|
||||||
color: #c87905;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 900;
|
|
||||||
padding: 6px 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-summary {
|
.source-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: end;
|
justify-items: end;
|
||||||
@@ -1425,14 +1408,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field__hint {
|
|
||||||
display: block;
|
|
||||||
min-height: 16px;
|
|
||||||
color: var(--th-color-slate-500);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory-control {
|
.directory-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ M009 后端 CP2 已实现:页面可不依赖订单或任务,用户手工填
|
|||||||
|
|
||||||
- 前端 V1 已按 `invoice.html` 的业务排版和字段关系实现为项目内页面,但不是直接上线原始 HTML。
|
- 前端 V1 已按 `invoice.html` 的业务排版和字段关系实现为项目内页面,但不是直接上线原始 HTML。
|
||||||
- 前端 V1 已新增 `/reservation/invoices/new` 页面,按 `RESERVATION_INVOICE_GENERATE` 路由权限保护,使用登录态 Bearer token 调用正式业务接口。
|
- 前端 V1 已新增 `/reservation/invoices/new` 页面,按 `RESERVATION_INVOICE_GENERATE` 路由权限保护,使用登录态 Bearer token 调用正式业务接口。
|
||||||
- 页面第一版保留原型中的英文业务字段标签,暂不新增 i18n;后续若 Manual Invoice 纳入三语言范围,再统一接入语言包。
|
- 页面标题、关键输入占位符和主要操作按钮已接入中 / 英 / 泰 i18n;第一版仍保留原型中的部分英文业务字段标签,后续若 Manual Invoice 全量纳入三语言范围,再统一清理。
|
||||||
- 页面已支持无订单 / 无任务独立生成,固定提交 `source_type=MANUAL`,`order_id=null`,`task_id=null`;暂不做订单 / 任务自动预填。
|
- 页面已支持无订单 / 无任务独立生成,固定提交 `source_type=MANUAL`,`order_id=null`,`task_id=null`;暂不做订单 / 任务自动预填。
|
||||||
- 侧边栏入口不由前端硬编码公开,仍依赖后端登录态 `menus[]`。如需菜单中展示,建议菜单管理配置 `menu_code=RESERVATION_MANUAL_INVOICE`、`route_path=/reservation/invoices/new`、`permission_code=RESERVATION_INVOICE_GENERATE`。
|
- 侧边栏入口不由前端硬编码公开,仍依赖后端登录态 `menus[]`。如需菜单中展示,建议菜单管理配置 `menu_code=RESERVATION_MANUAL_INVOICE`、`route_path=/reservation/invoices/new`、`permission_code=RESERVATION_INVOICE_GENERATE`。
|
||||||
- 第一阶段入口建议是独立页面,例如 `/reservation/invoices/new` 或 `/invoices/new`;不要求必须从任务详情或订单详情进入。
|
- 第一阶段入口建议是独立页面,例如 `/reservation/invoices/new` 或 `/invoices/new`;不要求必须从任务详情或订单详情进入。
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ server/src/main/resources/templates/reservation-invoice/proforma-invoice-v1.xlsx
|
|||||||
### CP3:前端 Manual Invoice 页面(已完成 V1)
|
### CP3:前端 Manual Invoice 页面(已完成 V1)
|
||||||
|
|
||||||
- 已完成:基于当前 `invoice.html` 原型改造成前端项目内 `/reservation/invoices/new` 页面。
|
- 已完成:基于当前 `invoice.html` 原型改造成前端项目内 `/reservation/invoices/new` 页面。
|
||||||
- 已完成:页面按 `01 Basic Information`、`02 Please refer to the following reservation`、`03 Description&Amount` 三段式结构展示,保留原型字段标签;第一版暂不新增 i18n。
|
- 已完成:页面按 `01 Basic Information`、`02 Please refer to the following reservation`、`03 Description&Amount` 三段式结构展示;标题、关键输入占位符和主要操作按钮已接入中 / 英 / 泰 i18n,其余原型字段标签后续按需求统一处理。
|
||||||
- 已完成:字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现,第一条费用明细和预订摘要区保持联动。
|
- 已完成:字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现,第一条费用明细和预订摘要区保持联动。
|
||||||
- 已完成:提交到后端业务接口,不调用调试上传接口。
|
- 已完成:提交到后端业务接口,不调用调试上传接口。
|
||||||
- 已完成:展示 PDF 生成结果、错误提示和下载 / 预览入口。
|
- 已完成:展示 PDF 生成结果、错误提示和下载 / 预览入口。
|
||||||
|
|||||||
Reference in New Issue
Block a user