对齐手工开票页面原型

This commit is contained in:
andy
2026-07-17 14:23:05 +07:00
parent 9ec13602e4
commit 94484dbf14
5 changed files with 1174 additions and 537 deletions

View File

@@ -209,6 +209,97 @@ describe('ReservationManualInvoiceView', () => {
)
})
it('renders the invoice.html style page sections and sticky action bar', () => {
const wrapper = mountView()
expect(wrapper.find('[data-testid="manual-invoice-prototype-page"]').exists()).toBe(true)
expect(wrapper.text()).toContain('创建 Invoice')
expect(wrapper.text()).toContain('01')
expect(wrapper.text()).toContain('Basic Information')
expect(wrapper.text()).toContain('02')
expect(wrapper.text()).toContain('Please refer to the following reservation')
expect(wrapper.text()).toContain('03')
expect(wrapper.text()).toContain('Description&Amount')
expect(wrapper.find('[data-testid="manual-invoice-action-bar"]').exists()).toBe(true)
})
it('shows final-format date previews like the invoice prototype', async () => {
const wrapper = mountView()
await wrapper.find('[data-testid="booking-arrival-date"]').setValue('2026-08-02')
await wrapper.find('[data-testid="booking-departure-date"]').setValue('2026-08-05')
await wrapper.find('[data-testid="document-due-date"]').setValue('2026-07-22')
expect(wrapper.find('[data-testid="arrival-date-preview"]').text()).toContain('02/08/2026')
expect(wrapper.find('[data-testid="departure-date-preview"]').text()).toContain('05/08/2026')
expect(wrapper.find('[data-testid="due-date-preview"]').text()).toContain('22/07/2026')
})
it('keeps primary reservation controls bound to the first charge line', async () => {
const wrapper = mountView()
await wrapper.find('[data-testid="primary-room-rate"]').setValue('3200')
await wrapper.find('[data-testid="primary-room-count"]').setValue('4')
await wrapper.find('[data-testid="primary-night-count"]').setValue('3')
expect((wrapper.find('[data-testid="charge-rate-0"]').element as HTMLInputElement).value).toBe('3200')
expect((wrapper.find('[data-testid="charge-quantity-0"]').element as HTMLInputElement).value).toBe('4')
expect((wrapper.find('[data-testid="charge-nights-0"]').element as HTMLInputElement).value).toBe('3')
expect(wrapper.find('[data-testid="charge-amount-0"]').text()).toContain('38,400.00')
})
it('submits comma-formatted numeric inputs as normalized numbers', async () => {
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
const wrapper = mountView()
await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD')
await fillMinimumInvoiceForm(wrapper)
await wrapper.find('[data-testid="booking-extra-bed-rate"]').setValue('1,200')
await wrapper.find('[data-testid="charge-rate-0"]').setValue('3,000')
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
await flushPromises()
expect(service.generateManualReservationInvoice).toHaveBeenCalledWith(
expect.objectContaining({
invoice_payload: expect.objectContaining({
booking: expect.objectContaining({
extra_bed_rate: 1200,
}),
charges: [
expect.objectContaining({
rate: 3000,
}),
],
}),
}),
)
})
it('clears directory contact text when switching company back to manual input', async () => {
const wrapper = mountView()
await wrapper.find('[data-testid="recipient-company-code"]').setValue('HANATOUR')
await wrapper.find('[data-testid="recipient-contact-id"]').setValue('HANATOUR_BOLAM_JO')
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe(
'melissa0609@hanatour.com',
)
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('input[placeholder="输入 Company"]').element as HTMLInputElement).value).toBe('')
expect((wrapper.find('input[placeholder="输入 Attention"]').element as HTMLInputElement).value).toBe('')
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe('')
})
it('keeps invoice date out of the keyboard focus order while preserving the payload field', () => {
const wrapper = mountView()
const invoiceDate = wrapper.find('[data-testid="document-invoice-date"]')
expect(invoiceDate.attributes('type')).toBe('hidden')
expect(invoiceDate.attributes('required')).toBeUndefined()
})
it('links company and attention seed data while allowing manual overrides before submit', async () => {
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
const wrapper = mountView()

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@
- 用户 / 权限底座后端 CP1 已完成;前端登录页、动态菜单、管理后台和业务审计 actor 全量迁移仍后置。
- 真实 OPERA / OHIP 接入后置;当前仅有 OPERA 模拟骨架。
- 任务卡字段控件契约 V1 后端第一版已完成,任务详情 `fields[]` 已返回 `control_type/edit_scope/write_target/options_source/raw_readonly/control_hint`;前端后续按契约接入,不要硬编码 PMS 房型、Rate Code 或未冻结枚举。
- Manual Invoice 第一阶段按 M009 推进:后端已提供 `POST /api/reservation/invoices/manual-generations`,前端已新增 `/reservation/invoices/new` 手工开票页面,页面可以不依赖订单或任务,用户手工填写 / 选择字段后由后端业务接口填充 Excel 模板并生成 PDF前端不得直接调用 M008 的调试上传转换接口来完成业务开票。侧边栏入口仍以登录后端返回的 menus 为准,建议后续在菜单管理中配置 `RESERVATION_MANUAL_INVOICE` / `/reservation/invoices/new` / `RESERVATION_INVOICE_GENERATE`
- Manual Invoice 第一阶段按 M009 推进:后端已提供 `POST /api/reservation/invoices/manual-generations`,前端已新增 `/reservation/invoices/new` 手工开票页面,并已按 `invoice.html` 原型的三段式业务结构对齐;页面可以不依赖订单或任务,用户手工填写 / 选择字段后由后端业务接口填充 Excel 模板并生成 PDF前端不得直接调用 M008 的调试上传转换接口来完成业务开票。侧边栏入口仍以登录后端返回的 menus 为准,建议后续在菜单管理中配置 `RESERVATION_MANUAL_INVOICE` / `/reservation/invoices/new` / `RESERVATION_INVOICE_GENERATE`
## 6. 前端开发注意事项

View File

@@ -383,8 +383,9 @@ M009 后端 CP2 已实现:页面可不依赖订单或任务,用户手工填
前端注意:
- `invoice.html` 可以作为交互原型和控件参考,但不直接作为生产页面上线。
-端 V1 已按 `invoice.html` 的业务排版和字段关系实现为项目内页面,但不直接上线原始 HTML
- 前端 V1 已新增 `/reservation/invoices/new` 页面,按 `RESERVATION_INVOICE_GENERATE` 路由权限保护,使用登录态 Bearer token 调用正式业务接口。
- 页面第一版保留原型中的英文业务字段标签,暂不新增 i18n后续若 Manual Invoice 纳入三语言范围,再统一接入语言包。
- 页面已支持无订单 / 无任务独立生成,固定提交 `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`
- 第一阶段入口建议是独立页面,例如 `/reservation/invoices/new``/invoices/new`;不要求必须从任务详情或订单详情进入。

View File

@@ -2,9 +2,9 @@
| 项目 | 内容 |
| --- | --- |
| 文档状态 | CP2 后端已实现,前端 CP3 待开发 |
| 文档状态 | CP3 前端 V1 已完成,后续继续推进订单 / 任务预填、客户目录接口和历史查询 |
| 适用范围 | 无订单 / 无任务场景下的手工 Invoice 创建,以及后续从任务或订单预填 Invoice |
| 当前目标 | 第一阶段基于现有 `invoice.html` 原型落地 Manual Invoice Mode后端已支持手工生成 PDF前端页面接入 |
| 当前目标 | 第一阶段基于现有 `invoice.html` 原型落地 Manual Invoice Mode前端页面接入正式业务接口并按原型三段式结构展示 |
| 依赖能力 | M008 Excel 转 PDF 平台转换能力、阿里云 OSS、登录权限和酒店上下文 |
## 1. 背景
@@ -363,12 +363,13 @@ server/src/main/resources/templates/reservation-invoice/proforma-invoice-v1.xlsx
- 已完成:失败记录保留已上传成功的 Excel / PDF object key业务审计完成后才标记 `SUCCEEDED`
- 第一版限制:只支持 `source_type=MANUAL``template_code=PROFORMA_INVOICE_V1`、最多 10 条费用明细;暂不提供历史列表、详情查询、任务 / 订单预填和客户联系人目录查询接口。
### CP3前端 Manual Invoice 页面
### CP3前端 Manual Invoice 页面(已完成 V1
- 基于当前 `invoice.html` 原型改造成前端项目内页面。
- 字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现
- 提交到后端业务接口,不调用调试上传接口
- 展示 PDF 生成结果、错误提示和下载 / 预览入口。
- 已完成:基于当前 `invoice.html` 原型改造成前端项目内 `/reservation/invoices/new` 页面。
- 已完成:页面按 `01 Basic Information``02 Please refer to the following reservation``03 Description&Amount` 三段式结构展示,保留原型字段标签;第一版暂不新增 i18n
- 已完成:字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现,第一条费用明细和预订摘要区保持联动
- 已完成:提交到后端业务接口,不调用调试上传接口。
- 已完成:展示 PDF 生成结果、错误提示和下载 / 预览入口。
### CP4任务 / 订单预填