增加选项及日期显示优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
v-model="dateModel"
|
v-model="dateModel"
|
||||||
:input-id="inputId"
|
:input-id="inputId"
|
||||||
class="localized-date-picker"
|
class="localized-date-picker"
|
||||||
date-format="yy-mm-dd"
|
:date-format="dateFormat"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:input-class="inputClass"
|
:input-class="inputClass"
|
||||||
:pt="datePickerPt"
|
:pt="datePickerPt"
|
||||||
@@ -36,6 +36,7 @@ const props = withDefaults(defineProps<{
|
|||||||
name?: string
|
name?: string
|
||||||
testId: string
|
testId: string
|
||||||
panelTestId?: string
|
panelTestId?: string
|
||||||
|
dateFormat?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
inputClass?: string | Record<string, boolean>
|
inputClass?: string | Record<string, boolean>
|
||||||
invalid?: boolean
|
invalid?: boolean
|
||||||
@@ -46,6 +47,7 @@ const props = withDefaults(defineProps<{
|
|||||||
}>(), {
|
}>(), {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
panelTestId: undefined,
|
panelTestId: undefined,
|
||||||
|
dateFormat: 'yy-mm-dd',
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
inputClass: undefined,
|
inputClass: undefined,
|
||||||
invalid: false,
|
invalid: false,
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export default {
|
|||||||
peoplePerRoom: 'People per room',
|
peoplePerRoom: 'People per room',
|
||||||
arrival: 'Arrival',
|
arrival: 'Arrival',
|
||||||
departure: 'Departure',
|
departure: 'Departure',
|
||||||
datePlaceholder: 'yyyy-MM-dd',
|
datePlaceholder: 'dd/MM/yyyy',
|
||||||
roomType: 'Room type',
|
roomType: 'Room type',
|
||||||
roomTypePlaceholder: 'e.g. RM2/RM3',
|
roomTypePlaceholder: 'e.g. RM2/RM3',
|
||||||
titleColumn: 'Title',
|
titleColumn: 'Title',
|
||||||
@@ -441,7 +441,7 @@ export default {
|
|||||||
nonNegativeInteger: 'Must be 0 or a positive integer.',
|
nonNegativeInteger: 'Must be 0 or a positive integer.',
|
||||||
arrivalRequired: 'Enter arrival date.',
|
arrivalRequired: 'Enter arrival date.',
|
||||||
departureRequired: 'Enter departure date.',
|
departureRequired: 'Enter departure date.',
|
||||||
dateFormat: 'Date format must be yyyy-MM-dd.',
|
dateFormat: 'Date format must be dd/MM/yyyy.',
|
||||||
departureAfterArrival: 'Departure must be later than arrival.',
|
departureAfterArrival: 'Departure must be later than arrival.',
|
||||||
roomTypeRequired: 'Enter room type.',
|
roomTypeRequired: 'Enter room type.',
|
||||||
paymentTypeRequired: 'Select payment type.',
|
paymentTypeRequired: 'Select payment type.',
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export default {
|
|||||||
peoplePerRoom: 'จำนวนคนต่อห้อง',
|
peoplePerRoom: 'จำนวนคนต่อห้อง',
|
||||||
arrival: 'Arrival',
|
arrival: 'Arrival',
|
||||||
departure: 'Departure',
|
departure: 'Departure',
|
||||||
datePlaceholder: 'yyyy-MM-dd',
|
datePlaceholder: 'dd/MM/yyyy',
|
||||||
roomType: 'Room Type',
|
roomType: 'Room Type',
|
||||||
roomTypePlaceholder: 'เช่น RM2/RM3',
|
roomTypePlaceholder: 'เช่น RM2/RM3',
|
||||||
titleColumn: 'Title',
|
titleColumn: 'Title',
|
||||||
@@ -441,7 +441,7 @@ export default {
|
|||||||
nonNegativeInteger: 'ต้องเป็น 0 หรือจำนวนเต็มบวก',
|
nonNegativeInteger: 'ต้องเป็น 0 หรือจำนวนเต็มบวก',
|
||||||
arrivalRequired: 'กรุณากรอก Arrival',
|
arrivalRequired: 'กรุณากรอก Arrival',
|
||||||
departureRequired: 'กรุณากรอก Departure',
|
departureRequired: 'กรุณากรอก Departure',
|
||||||
dateFormat: 'รูปแบบวันที่ต้องเป็น yyyy-MM-dd',
|
dateFormat: 'รูปแบบวันที่ต้องเป็น dd/MM/yyyy',
|
||||||
departureAfterArrival: 'Departure ต้องอยู่หลัง Arrival',
|
departureAfterArrival: 'Departure ต้องอยู่หลัง Arrival',
|
||||||
roomTypeRequired: 'กรุณากรอก Room Type',
|
roomTypeRequired: 'กรุณากรอก Room Type',
|
||||||
paymentTypeRequired: 'กรุณาเลือก Payment Type',
|
paymentTypeRequired: 'กรุณาเลือก Payment Type',
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ export default {
|
|||||||
peoplePerRoom: '每间房人数',
|
peoplePerRoom: '每间房人数',
|
||||||
arrival: '入住日期',
|
arrival: '入住日期',
|
||||||
departure: '离店日期',
|
departure: '离店日期',
|
||||||
datePlaceholder: 'yyyy-MM-dd',
|
datePlaceholder: 'dd/MM/yyyy',
|
||||||
roomType: '房型',
|
roomType: '房型',
|
||||||
roomTypePlaceholder: '例如 RM2/RM3',
|
roomTypePlaceholder: '例如 RM2/RM3',
|
||||||
titleColumn: '称谓(Title)',
|
titleColumn: '称谓(Title)',
|
||||||
@@ -440,7 +440,7 @@ export default {
|
|||||||
nonNegativeInteger: '必须是 0 或正整数。',
|
nonNegativeInteger: '必须是 0 或正整数。',
|
||||||
arrivalRequired: '请填写入住日期。',
|
arrivalRequired: '请填写入住日期。',
|
||||||
departureRequired: '请填写离店日期。',
|
departureRequired: '请填写离店日期。',
|
||||||
dateFormat: '日期格式必须为 yyyy-MM-dd。',
|
dateFormat: '日期格式必须为 dd/MM/yyyy。',
|
||||||
departureAfterArrival: '离店日期必须晚于入住日期。',
|
departureAfterArrival: '离店日期必须晚于入住日期。',
|
||||||
roomTypeRequired: '请填写房型。',
|
roomTypeRequired: '请填写房型。',
|
||||||
paymentTypeRequired: '请选择付款方式。',
|
paymentTypeRequired: '请选择付款方式。',
|
||||||
|
|||||||
@@ -182,8 +182,16 @@ const menuLabelKeys: Record<string, string> = {
|
|||||||
SYSTEM_RESERVATION_CATALOGS: 'nav.systemReservationCatalogs',
|
SYSTEM_RESERVATION_CATALOGS: 'nav.systemReservationCatalogs',
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = computed(() => authStore.visibleMenus)
|
const defaultSidebarMenuCodes = new Set([
|
||||||
const brandTarget = computed(() => resolveAuthenticatedEntryPath(authStore.firstMenuPath))
|
'RESERVATION_MANUAL_INVOICE',
|
||||||
|
'RESERVATION_INVOICES',
|
||||||
|
'RESERVATION_ROOMING_LIST',
|
||||||
|
'RESERVATION_ROOMING_LISTS',
|
||||||
|
'SYSTEM_SETTINGS',
|
||||||
|
])
|
||||||
|
|
||||||
|
const navItems = computed(() => authStore.visibleMenus.filter((menu) => defaultSidebarMenuCodes.has(menu.menu_code)))
|
||||||
|
const brandTarget = computed(() => resolveAuthenticatedEntryPath(navItems.value[0]?.route_path ?? authStore.firstMenuPath))
|
||||||
const currentUserLabel = computed(() => authStore.user?.display_name || authStore.user?.username || '-')
|
const currentUserLabel = computed(() => authStore.user?.display_name || authStore.user?.username || '-')
|
||||||
const currentHotelLabel = computed(() => {
|
const currentHotelLabel = computed(() => {
|
||||||
const hotel = authStore.selectedHotel
|
const hotel = authStore.selectedHotel
|
||||||
|
|||||||
@@ -573,6 +573,99 @@ describe('ReservationManualInvoiceView', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('prefills TEIGER TRAVEL directory data and submits its stable recipient codes', async () => {
|
||||||
|
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
||||||
|
const wrapper = mountView()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="recipient-company-code"]').setValue('TEIGER_TRAVEL')
|
||||||
|
|
||||||
|
expect((wrapper.find('[data-testid="recipient-contact-id"]').element as HTMLSelectElement).value).toBe(
|
||||||
|
'TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-company-input"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'TEIGER TRAVEL GROUP CO.,LTD',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-attention-input"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'Khun Nutkritta Upamai (Kwan)',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-address"]').element as HTMLTextAreaElement).value).toBe(
|
||||||
|
'20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-telephone"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'063-516-8294',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'nutkritta.upamai@gmail.com',
|
||||||
|
)
|
||||||
|
|
||||||
|
await fillMinimumInvoiceForm(wrapper)
|
||||||
|
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(service.generateManualReservationInvoice).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
invoice_payload: expect.objectContaining({
|
||||||
|
recipient: expect.objectContaining({
|
||||||
|
company_code: 'TEIGER_TRAVEL',
|
||||||
|
contact_id: 'TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI',
|
||||||
|
company: 'TEIGER TRAVEL GROUP CO.,LTD',
|
||||||
|
attention: 'Khun Nutkritta Upamai (Kwan)',
|
||||||
|
address: '20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250',
|
||||||
|
telephone: '063-516-8294',
|
||||||
|
email: 'nutkritta.upamai@gmail.com',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefills CLOVER HOLIDAY directory data and submits its stable recipient codes', async () => {
|
||||||
|
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
||||||
|
const wrapper = mountView()
|
||||||
|
|
||||||
|
await wrapper.find('[data-testid="recipient-company-code"]').setValue('CLOVER_HOLIDAY')
|
||||||
|
|
||||||
|
expect((wrapper.find('[data-testid="recipient-contact-id"]').element as HTMLSelectElement).value).toBe(
|
||||||
|
'CLOVER_HOLIDAY_KHUN_JANE',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-company-input"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'CLOVER HOLIDAY',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-attention-input"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'KHUN JANE',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-address"]').element as HTMLTextAreaElement).value).toBe(
|
||||||
|
'290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-telephone"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'083-1470352',
|
||||||
|
)
|
||||||
|
expect((wrapper.find('[data-testid="recipient-email"]').element as HTMLInputElement).value).toBe(
|
||||||
|
'longtai.maleemalee@gmail.com',
|
||||||
|
)
|
||||||
|
|
||||||
|
await fillMinimumInvoiceForm(wrapper)
|
||||||
|
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(service.generateManualReservationInvoice).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
invoice_payload: expect.objectContaining({
|
||||||
|
recipient: expect.objectContaining({
|
||||||
|
company_code: 'CLOVER_HOLIDAY',
|
||||||
|
contact_id: 'CLOVER_HOLIDAY_KHUN_JANE',
|
||||||
|
company: 'CLOVER HOLIDAY',
|
||||||
|
attention: 'KHUN JANE',
|
||||||
|
address:
|
||||||
|
'290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip',
|
||||||
|
telephone: '083-1470352',
|
||||||
|
email: 'longtai.maleemalee@gmail.com',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('downloads the generated PDF through a browser blob URL', async () => {
|
it('downloads the generated PDF through a browser blob URL', async () => {
|
||||||
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
vi.mocked(service.generateManualReservationInvoice).mockResolvedValue(createResult())
|
||||||
const fetchMock = vi.fn().mockResolvedValue({
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -194,21 +194,29 @@ describe('ReservationAppShell', () => {
|
|||||||
expect(wrapper.text()).toContain('Order list')
|
expect(wrapper.text()).toContain('Order list')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders navigation from backend menus and hides Debug EML when the menu is absent', async () => {
|
it('limits the default sidebar to manual invoice, rooming list, and system settings', async () => {
|
||||||
const wrapper = await mountShell()
|
const wrapper = await mountShell([
|
||||||
|
createReservationOrdersMenu(),
|
||||||
|
createReservationTasksMenu(),
|
||||||
|
createDebugMenu(),
|
||||||
|
createManualInvoiceMenu(),
|
||||||
|
createRoomingListMenu(),
|
||||||
|
createSystemMenu(),
|
||||||
|
])
|
||||||
|
|
||||||
expect(wrapper.find('a[href="/reservation/orders"]').exists()).toBe(true)
|
expect(wrapper.find('nav a[href="/reservation/invoices/new"]').exists()).toBe(true)
|
||||||
expect(wrapper.find('a[href="/reservation/tasks"]').exists()).toBe(true)
|
expect(wrapper.find('nav a[href="/reservation/rooming-lists/new"]').exists()).toBe(true)
|
||||||
expect(wrapper.text()).toContain('预订事项')
|
expect(wrapper.find('nav a[href="/system"]').exists()).toBe(true)
|
||||||
expect(wrapper.text()).not.toContain('任务队列')
|
expect(wrapper.find('nav a[href="/reservation/orders"]').exists()).toBe(false)
|
||||||
expect(wrapper.find('a[href="/debug/eml-superagent"]').exists()).toBe(false)
|
expect(wrapper.find('nav a[href="/reservation/tasks"]').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('nav a[href="/debug/eml-superagent"]').exists()).toBe(false)
|
||||||
|
expect(wrapper.findAll('nav a')).toHaveLength(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows Debug EML only when backend menus include it', async () => {
|
it('hides Debug EML from the default sidebar even when backend menus include it', async () => {
|
||||||
const wrapper = await mountShell([createReservationOrdersMenu(), createDebugMenu()])
|
const wrapper = await mountShell([createReservationOrdersMenu(), createDebugMenu()])
|
||||||
|
|
||||||
expect(wrapper.find('a[href="/debug/eml-superagent"]').exists()).toBe(true)
|
expect(wrapper.find('nav a[href="/debug/eml-superagent"]').exists()).toBe(false)
|
||||||
expect(wrapper.text()).toContain('Debug EML')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows Manual Invoice only when backend menus include it', async () => {
|
it('shows Manual Invoice only when backend menus include it', async () => {
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ describe('ReservationRoomingListGenerationView', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('[data-testid="rooming-list-arrival"]').exists()).toBe(true)
|
expect(wrapper.find('[data-testid="rooming-list-arrival"]').exists()).toBe(true)
|
||||||
expect(wrapper.find('[data-testid="rooming-list-departure"]').exists()).toBe(true)
|
expect(wrapper.find('[data-testid="rooming-list-departure"]').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('[data-testid="rooming-list-arrival"]').attributes('placeholder')).toBe('dd/MM/yyyy')
|
||||||
|
expect(wrapper.find('[data-testid="rooming-list-departure"]').attributes('placeholder')).toBe('dd/MM/yyyy')
|
||||||
expect(wrapper.find('[data-testid="rooming-list-room-type"]').attributes('placeholder')).toBe('例如 RM2/RM3')
|
expect(wrapper.find('[data-testid="rooming-list-room-type"]').attributes('placeholder')).toBe('例如 RM2/RM3')
|
||||||
expect(wrapper.find('[data-testid="rooming-list-title"]').exists()).toBe(false)
|
expect(wrapper.find('[data-testid="rooming-list-title"]').exists()).toBe(false)
|
||||||
expect(wrapper.find('[data-testid="rooming-list-rate-code"]').exists()).toBe(false)
|
expect(wrapper.find('[data-testid="rooming-list-rate-code"]').exists()).toBe(false)
|
||||||
@@ -247,8 +249,8 @@ describe('ReservationRoomingListGenerationView', () => {
|
|||||||
})
|
})
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
await fillRequiredFields(wrapper)
|
await fillRequiredFields(wrapper)
|
||||||
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('2026-05-10')
|
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('10/05/2026')
|
||||||
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('2026-05-16')
|
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('16/05/2026')
|
||||||
|
|
||||||
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -264,8 +266,8 @@ describe('ReservationRoomingListGenerationView', () => {
|
|||||||
it('blocks submit when optional manual stay dates are incomplete or out of order', async () => {
|
it('blocks submit when optional manual stay dates are incomplete or out of order', async () => {
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
await fillRequiredFields(wrapper)
|
await fillRequiredFields(wrapper)
|
||||||
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('2026-05-16')
|
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('16/05/2026')
|
||||||
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('2026-05-10')
|
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('10/05/2026')
|
||||||
|
|
||||||
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -357,7 +359,7 @@ describe('ReservationRoomingListGenerationView', () => {
|
|||||||
|
|
||||||
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
await wrapper.find('[data-testid="rooming-list-submit"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('2026-05-10')
|
await wrapper.find('[data-testid="rooming-list-arrival"]').setValue('10/05/2026')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.text()).not.toContain('Enter arrival date.')
|
expect(wrapper.text()).not.toContain('Enter arrival date.')
|
||||||
@@ -365,7 +367,7 @@ describe('ReservationRoomingListGenerationView', () => {
|
|||||||
expect(wrapper.find('[data-testid="rooming-list-arrival"]').classes()).not.toContain('is-invalid')
|
expect(wrapper.find('[data-testid="rooming-list-arrival"]').classes()).not.toContain('is-invalid')
|
||||||
expect(wrapper.find('[data-testid="rooming-list-departure"]').classes()).toContain('is-invalid')
|
expect(wrapper.find('[data-testid="rooming-list-departure"]').classes()).toContain('is-invalid')
|
||||||
|
|
||||||
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('2026-05-16')
|
await wrapper.find('[data-testid="rooming-list-departure"]').setValue('16/05/2026')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.text()).not.toContain('Enter departure date.')
|
expect(wrapper.text()).not.toContain('Enter departure date.')
|
||||||
|
|||||||
@@ -883,6 +883,32 @@ const recipientCompanies: RecipientCompanySeed[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
company_code: 'TEIGER_TRAVEL',
|
||||||
|
company: 'TEIGER TRAVEL GROUP CO.,LTD',
|
||||||
|
contacts: [
|
||||||
|
{
|
||||||
|
contact_id: 'TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI',
|
||||||
|
attention: 'Khun Nutkritta Upamai (Kwan)',
|
||||||
|
address: '20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250',
|
||||||
|
telephone: '063-516-8294',
|
||||||
|
email: 'nutkritta.upamai@gmail.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
company_code: 'CLOVER_HOLIDAY',
|
||||||
|
company: 'CLOVER HOLIDAY',
|
||||||
|
contacts: [
|
||||||
|
{
|
||||||
|
contact_id: 'CLOVER_HOLIDAY_KHUN_JANE',
|
||||||
|
attention: 'KHUN JANE',
|
||||||
|
address: '290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip',
|
||||||
|
telephone: '083-1470352',
|
||||||
|
email: 'longtai.maleemalee@gmail.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
company_code: 'HANATOUR',
|
company_code: 'HANATOUR',
|
||||||
company: 'HANATOUR TD CO., LTD.',
|
company: 'HANATOUR TD CO., LTD.',
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
v-model="form.arrival"
|
v-model="form.arrival"
|
||||||
input-id="rooming-list-arrival-input"
|
input-id="rooming-list-arrival-input"
|
||||||
test-id="rooming-list-arrival"
|
test-id="rooming-list-arrival"
|
||||||
|
date-format="dd/mm/yy"
|
||||||
:placeholder="t('roomingList.datePlaceholder')"
|
:placeholder="t('roomingList.datePlaceholder')"
|
||||||
:input-class="validationClass('arrival')"
|
:input-class="validationClass('arrival')"
|
||||||
:invalid="Boolean(fieldError('arrival'))"
|
:invalid="Boolean(fieldError('arrival'))"
|
||||||
@@ -153,6 +154,7 @@
|
|||||||
v-model="form.departure"
|
v-model="form.departure"
|
||||||
input-id="rooming-list-departure-input"
|
input-id="rooming-list-departure-input"
|
||||||
test-id="rooming-list-departure"
|
test-id="rooming-list-departure"
|
||||||
|
date-format="dd/mm/yy"
|
||||||
:placeholder="t('roomingList.datePlaceholder')"
|
:placeholder="t('roomingList.datePlaceholder')"
|
||||||
:input-class="validationClass('departure')"
|
:input-class="validationClass('departure')"
|
||||||
:invalid="Boolean(fieldError('departure'))"
|
:invalid="Boolean(fieldError('departure'))"
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ POST /api/auth/logout
|
|||||||
- 邮件原文 / conversation 完整正文接口已完成权限收口,必须带 Bearer token 且同时需要 `SOURCE_MESSAGE_READ` 和 `SOURCE_MESSAGE_ORIGINAL_READ`;Reservation 写操作、Debug / Demo / Replay / Probe 等接口仍按 `../security-access-control-boundary.md` 的分阶段计划继续收口。
|
- 邮件原文 / conversation 完整正文接口已完成权限收口,必须带 Bearer token 且同时需要 `SOURCE_MESSAGE_READ` 和 `SOURCE_MESSAGE_ORIGINAL_READ`;Reservation 写操作、Debug / Demo / Replay / Probe 等接口仍按 `../security-access-control-boundary.md` 的分阶段计划继续收口。
|
||||||
- `/api/auth/me` 返回 `user`、`default_hotel_id`、`hotels[]`、`permissions[]`、`menus[]`;菜单入口应优先使用 `menus[]`,不要继续硬编码订单列表、任务队列、Debug EML。
|
- `/api/auth/me` 返回 `user`、`default_hotel_id`、`hotels[]`、`permissions[]`、`menus[]`;菜单入口应优先使用 `menus[]`,不要继续硬编码订单列表、任务队列、Debug EML。
|
||||||
- `menus[]` 只包含可见菜单;订单详情、任务详情和邮件会话详情是隐藏详情路由,不会作为菜单项返回。
|
- `menus[]` 只包含可见菜单;订单详情、任务详情和邮件会话详情是隐藏详情路由,不会作为菜单项返回。
|
||||||
|
- 当前 OMS 左侧默认侧边栏只展示 `RESERVATION_MANUAL_INVOICE`、`RESERVATION_ROOMING_LIST`、`SYSTEM_SETTINGS` 三个产品入口;后端仍可在 `menus[]` 返回订单、任务、Debug 或管理子菜单用于权限、直达路由和系统管理维护,前端默认导航会过滤隐藏这些入口。
|
||||||
- `DEBUG_EML_SUPERAGENT` 菜单第一版只授予 `SYSTEM_ADMIN`;这只表示页面入口是否可见,不代表后端会把 `X-TH-Hotel-Debug-Upload-Key` 下发给前端。
|
- `DEBUG_EML_SUPERAGENT` 菜单第一版只授予 `SYSTEM_ADMIN`;这只表示页面入口是否可见,不代表后端会把 `X-TH-Hotel-Debug-Upload-Key` 下发给前端。
|
||||||
- `user.id` 是字符串;前端不要把任何后端 ID 转成 JavaScript number。
|
- `user.id` 是字符串;前端不要把任何后端 ID 转成 JavaScript number。
|
||||||
- 登录失败统一显示用户名或密码错误,不要根据错误文案推断账号是否存在或是否禁用。
|
- 登录失败统一显示用户名或密码错误,不要根据错误文案推断账号是否存在或是否禁用。
|
||||||
@@ -447,7 +448,7 @@ M009 后端 CP2 已实现:页面可不依赖订单或任务,用户手工填
|
|||||||
- 从任务进入时,后续可以使用 `GET /api/reservation/tasks/{taskId}` 的 `fields[]`、草稿或确认 payload 预填;从订单进入时,需要明确选择具体任务或提示仅使用订单摘要,避免一个订单多任务时字段来源不清。
|
- 从任务进入时,后续可以使用 `GET /api/reservation/tasks/{taskId}` 的 `fields[]`、草稿或确认 payload 预填;从订单进入时,需要明确选择具体任务或提示仅使用订单摘要,避免一个订单多任务时字段来源不清。
|
||||||
- Company、Attention、Address、Tel、Email、Booking Date 第一阶段可以按“选择 + Manual 手填”控件处理。
|
- Company、Attention、Address、Tel、Email、Booking Date 第一阶段可以按“选择 + Manual 手填”控件处理。
|
||||||
- Company、Attention、Address、Tel、Email 是一组收件方联系人档案,不是五个互相独立字段;选择 Company 后应刷新 Attention 候选,选择 Attention 后应带出 Address、Tel、Email。Booking Date 展示在同一区域,但不属于联系人档案,应作为 `document.booking_date` 独立提交。
|
- Company、Attention、Address、Tel、Email 是一组收件方联系人档案,不是五个互相独立字段;选择 Company 后应刷新 Attention 候选,选择 Attention 后应带出 Address、Tel、Email。Booking Date 展示在同一区域,但不属于联系人档案,应作为 `document.booking_date` 独立提交。
|
||||||
- 第一阶段已确认四组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。
|
- 第一阶段已确认六组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`TEIGER_TRAVEL` / `TEIGER TRAVEL GROUP CO.,LTD` + `Khun Nutkritta Upamai (Kwan)`;`CLOVER_HOLIDAY` / `CLOVER HOLIDAY` + `KHUN JANE`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。
|
||||||
- Room Type、Room Rate、Extra Bed 建议也预留“选择 + 可手填 / 可覆盖”能力,后续接 PMS 房型、Rate Code 或价格配置。
|
- Room Type、Room Rate、Extra Bed 建议也预留“选择 + 可手填 / 可覆盖”能力,后续接 PMS 房型、Rate Code 或价格配置。
|
||||||
- Room Rate / 费用明细 Rate 第一阶段只校验必填、数字格式和非负数,允许 `0`;Quantity 和 Nights 仍必须大于 `0`。
|
- Room Rate / 费用明细 Rate 第一阶段只校验必填、数字格式和非负数,允许 `0`;Quantity 和 Nights 仍必须大于 `0`。
|
||||||
- No.of Night(s) 前端默认按 `Departure Date - Arrival Date` 自动填入第一条费用明细 Night(s),用户手工修改后保留用户输入,不再因日期变化覆盖。
|
- No.of Night(s) 前端默认按 `Departure Date - Arrival Date` 自动填入第一条费用明细 Night(s),用户手工修改后保留用户输入,不再因日期变化覆盖。
|
||||||
|
|||||||
@@ -114,6 +114,8 @@
|
|||||||
| `LIAN_TAI` | `LIAN TAI TRAVEL (THAILAND) CO., LTD.` | `LIAN_TAI_KHUN_ANN` | `Khun Ann` | `2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240` | `061-397-2675` | `op.liantaitravel@gmail.com` |
|
| `LIAN_TAI` | `LIAN TAI TRAVEL (THAILAND) CO., LTD.` | `LIAN_TAI_KHUN_ANN` | `Khun Ann` | `2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240` | `061-397-2675` | `op.liantaitravel@gmail.com` |
|
||||||
| `QBD` | `Q.B.D. TRAVEL GROUP CO., LTD` | `QBD_JITDANUN_PANAPHUCHONG` | `Jitdanun Panaphuchong` | `2/90 Rajpattana,Rajpattana,Sapansoong, Bangkok, TH, 10240` | `089-032 0176` | `op.qbdtravel@gmail.com` |
|
| `QBD` | `Q.B.D. TRAVEL GROUP CO., LTD` | `QBD_JITDANUN_PANAPHUCHONG` | `Jitdanun Panaphuchong` | `2/90 Rajpattana,Rajpattana,Sapansoong, Bangkok, TH, 10240` | `089-032 0176` | `op.qbdtravel@gmail.com` |
|
||||||
| `MING_SONG` | `MING SONG CO.LTD` | `MING_SONG_KHUN_JIN` | `KHUN JIN` | `290 Soi Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, Wang Thonglang District, Bangkok, TH, 10310` | `098-2519896` | `taiziyoutravel@gmail.com` |
|
| `MING_SONG` | `MING SONG CO.LTD` | `MING_SONG_KHUN_JIN` | `KHUN JIN` | `290 Soi Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, Wang Thonglang District, Bangkok, TH, 10310` | `098-2519896` | `taiziyoutravel@gmail.com` |
|
||||||
|
| `TEIGER_TRAVEL` | `TEIGER TRAVEL GROUP CO.,LTD` | `TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI` | `Khun Nutkritta Upamai (Kwan)` | `20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250` | `063-516-8294` | `nutkritta.upamai@gmail.com` |
|
||||||
|
| `CLOVER_HOLIDAY` | `CLOVER HOLIDAY` | `CLOVER_HOLIDAY_KHUN_JANE` | `KHUN JANE` | `290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip` | `083-1470352` | `longtai.maleemalee@gmail.com` |
|
||||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_WICHIENPRAKARN` | `Wichienprakarn, Nuanphae, Khun.` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `066 124 - 7297` | `HI219@hanatour.com` |
|
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_WICHIENPRAKARN` | `Wichienprakarn, Nuanphae, Khun.` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `066 124 - 7297` | `HI219@hanatour.com` |
|
||||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_PHONGBUPPA` | `Phongbuppa, Buppachat` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `096 051 3587` | `HI223@hanatour.com` |
|
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_PHONGBUPPA` | `Phongbuppa, Buppachat` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `096 051 3587` | `HI223@hanatour.com` |
|
||||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_KANG_SUNG_HWA` | `Kang,Sung Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 051 804 0707` | `k8040707@nave.com` |
|
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_KANG_SUNG_HWA` | `Kang,Sung Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 051 804 0707` | `k8040707@nave.com` |
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ RESERVATION_ROOMING_LIST_GENERATE
|
|||||||
- 填写每间房人数。
|
- 填写每间房人数。
|
||||||
- 填写目标 `Room Type`。
|
- 填写目标 `Room Type`。
|
||||||
- 填写 `Arrival` / `Departure`:第一种来源样式可不填,无旅游日期来源样式必填;页面文案需说明“当来源名单没有旅游日期时请填写”。
|
- 填写 `Arrival` / `Departure`:第一种来源样式可不填,无旅游日期来源样式必填;页面文案需说明“当来源名单没有旅游日期时请填写”。
|
||||||
|
- `Arrival` / `Departure` 页面输入提示和显示格式为 `dd/MM/yyyy`,前端提交给后端的 multipart 字段仍为 `yyyy-MM-dd`。
|
||||||
- 目标默认值区域只保留 `Payment Type` 和 `Nationality`。
|
- 目标默认值区域只保留 `Payment Type` 和 `Nationality`。
|
||||||
- `Payment Type` 使用下拉选择,默认 `BTQR`,当前可选择 `BTQR` 或 `CA`。
|
- `Payment Type` 使用下拉选择,默认 `BTQR`,当前可选择 `BTQR` 或 `CA`。
|
||||||
- `Nationality` 使用下拉,只允许 `KR`、`CN`、`TH`、`MM`、`RS`、`TW`。
|
- `Nationality` 使用下拉,只允许 `KR`、`CN`、`TH`、`MM`、`RS`、`TW`。
|
||||||
@@ -400,6 +401,7 @@ RESERVATION_ROOMING_LIST_GENERATE
|
|||||||
- 不要在前端解析完整 Excel 作为权威结果;前端预览可以后置为后端 preview 接口。
|
- 不要在前端解析完整 Excel 作为权威结果;前端预览可以后置为后端 preview 接口。
|
||||||
- 第一版如果没有 preview 接口,可以只展示用户输入和文件名,生成成功后直接下载。
|
- 第一版如果没有 preview 接口,可以只展示用户输入和文件名,生成成功后直接下载。
|
||||||
- CP3 起页面重新展示并提交可选 `arrival`、`departure`;后端仅在无旅游日期来源样式下要求这两个字段必填,第一种来源样式仍优先使用来源 Excel `旅游日期`。
|
- CP3 起页面重新展示并提交可选 `arrival`、`departure`;后端仅在无旅游日期来源样式下要求这两个字段必填,第一种来源样式仍优先使用来源 Excel `旅游日期`。
|
||||||
|
- 日期控件可以显示 `dd/MM/yyyy`,但服务层入参必须继续使用 `yyyy-MM-dd`,避免改变后端接口契约。
|
||||||
- 前端不要自行读取 Excel 并推导日期;如果用户上传没有 `旅游日期` 的名单,由用户手工填写 `arrival` / `departure`。
|
- 前端不要自行读取 Excel 并推导日期;如果用户上传没有 `旅游日期` 的名单,由用户手工填写 `arrival` / `departure`。
|
||||||
- `Adults` 由后端按分房结果计算;前端不要让用户覆盖。
|
- `Adults` 由后端按分房结果计算;前端不要让用户覆盖。
|
||||||
- 不要把上传文件内容、客人名单或生成文件内容写进浏览器日志、错误上报、URL 或 localStorage。
|
- 不要把上传文件内容、客人名单或生成文件内容写进浏览器日志、错误上报、URL 或 localStorage。
|
||||||
|
|||||||
Reference in New Issue
Block a user