修复手工开票下载与错误提示

This commit is contained in:
andy
2026-07-17 12:49:58 +07:00
parent 72fee44a68
commit f4d86248d4
7 changed files with 301 additions and 18 deletions

View File

@@ -28,6 +28,7 @@
data-testid="document-invoice-date"
type="date"
required
@input="markDocumentDatesEdited"
>
</label>
<label class="field">
@@ -37,6 +38,7 @@
data-testid="document-booking-date"
type="date"
required
@input="markDocumentDatesEdited"
>
</label>
<label class="field">
@@ -46,6 +48,7 @@
data-testid="document-due-date"
type="date"
required
@input="markDocumentDatesEdited"
>
</label>
</div>
@@ -320,7 +323,7 @@
:class="`invoice-alert--${alertTone}`"
>
<strong>{{ alertTitle }}</strong>
<ul>
<ul data-testid="manual-invoice-user-messages">
<li
v-for="message in messages"
:key="message"
@@ -328,6 +331,21 @@
{{ message }}
</li>
</ul>
<details
v-if="technicalDetails.length > 0"
class="technical-details"
data-testid="manual-invoice-technical-details"
>
<summary>{{ t('manualInvoice.technicalDetails') }}</summary>
<ul>
<li
v-for="detail in technicalDetails"
:key="detail"
>
{{ detail }}
</li>
</ul>
</details>
</section>
<section
@@ -372,16 +390,25 @@
>
{{ t('manualInvoice.openPdf') }}
</a>
<a
<button
type="button"
class="secondary-link"
:href="generationResult.pdf_url"
download
data-testid="manual-invoice-download-pdf"
:disabled="downloadingPdf"
@click="downloadPdf"
>
{{ t('manualInvoice.downloadPdf') }}
</a>
{{ downloadingPdf ? t('manualInvoice.downloadingPdf') : t('manualInvoice.downloadPdf') }}
</button>
</div>
<p
v-else
v-if="downloadMessage"
class="muted-text"
data-testid="manual-invoice-download-message"
>
{{ downloadMessage }}
</p>
<p
v-if="!generationResult.pdf_url"
class="muted-text"
>
{{ t('manualInvoice.noPdfUrl') }}
@@ -450,6 +477,11 @@ interface ChargeForm {
nights: string
}
interface GenerationErrorDescription {
message: string
technicalDetails: string[]
}
const recipientCompanies: RecipientCompanySeed[] = [
{
company_code: 'LIAN_TAI',
@@ -565,9 +597,13 @@ const form = reactive({
})
const submitting = ref(false)
const downloadingPdf = ref(false)
const messages = ref<string[]>([])
const technicalDetails = ref<string[]>([])
const downloadMessage = ref('')
const alertTone = ref<'error' | 'success'>('error')
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
const documentDatesEdited = ref(false)
const recipientContacts = computed(() => {
return recipientCompanies.find((company) => company.company_code === form.recipient.company_code)?.contacts ?? []
@@ -614,6 +650,16 @@ watch(
},
)
watch(
() => authStore.selectedHotel?.time_zone,
(timeZone, previousTimeZone) => {
if (!timeZone || timeZone === previousTimeZone || documentDatesEdited.value) {
return
}
applyDefaultDocumentDates(timeZone)
},
)
function createCharge(): ChargeForm {
const id = nextChargeId
nextChargeId += 1
@@ -652,6 +698,8 @@ function lineAmount(charge: ChargeForm): number {
}
async function submitInvoice(): Promise<void> {
technicalDetails.value = []
downloadMessage.value = ''
messages.value = validateForm()
alertTone.value = 'error'
if (messages.value.length > 0) {
@@ -664,12 +712,40 @@ async function submitInvoice(): Promise<void> {
generationResult.value = await generateManualReservationInvoice(buildRequest())
messages.value = []
} catch (error) {
messages.value = describeGenerationError(error)
const errorDescription = describeGenerationError(error)
messages.value = [errorDescription.message]
technicalDetails.value = errorDescription.technicalDetails
} finally {
submitting.value = false
}
}
async function downloadPdf(): Promise<void> {
const result = generationResult.value
const pdfUrl = result?.pdf_url
if (!pdfUrl || downloadingPdf.value) {
return
}
downloadingPdf.value = true
downloadMessage.value = ''
try {
const response = await fetch(pdfUrl, { credentials: 'omit' })
if (!response.ok) {
throw new Error(`PDF download failed with status ${response.status}`)
}
const blob = await response.blob()
const objectUrl = URL.createObjectURL(blob)
triggerBrowserDownload(objectUrl, createPdfFileName(result.invoice_generation_id))
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
} catch {
downloadMessage.value = t('manualInvoice.downloadFallback')
window.open(pdfUrl, '_blank', 'noopener,noreferrer')
} finally {
downloadingPdf.value = false
}
}
function buildRequest(): ManualInvoiceGenerationRequest {
return {
hotel_id: authStore.selectedHotelId ?? undefined,
@@ -759,15 +835,21 @@ function validateForm(): string[] {
return [...new Set(errors)]
}
function describeGenerationError(error: unknown): string[] {
function describeGenerationError(error: unknown): GenerationErrorDescription {
if (error instanceof ApiError) {
const authKey = error.status === 401 ? 'AUTH_401' : error.status === 403 ? 'AUTH_403' : null
const errorCode = authKey ?? errorCodeFromDetails(error.details)
const messageKey = `manualInvoice.errors.${errorCode}`
const message = te(messageKey) ? t(messageKey) : t('manualInvoice.errors.UNKNOWN')
return [message, ...detailMessages(error.details)]
return {
message,
technicalDetails: detailMessages(error.details),
}
}
return {
message: t('manualInvoice.errors.UNKNOWN'),
technicalDetails: [],
}
return [t('manualInvoice.errors.UNKNOWN')]
}
function errorCodeFromDetails(details: unknown): string {
@@ -781,14 +863,15 @@ function detailMessages(details: unknown): string[] {
if (!isRecord(details)) {
return []
}
const messages: string[] = []
if (typeof details.message === 'string') {
messages.push(details.message)
}
const rawDetails = details.details
if (Array.isArray(rawDetails)) {
return rawDetails.filter((item): item is string => typeof item === 'string')
messages.push(...rawDetails.filter((item): item is string => typeof item === 'string'))
}
if (typeof details.message === 'string') {
return [details.message]
}
return []
return [...new Set(messages)]
}
function applyContactSeed(contactId: string): void {
@@ -802,6 +885,33 @@ function applyContactSeed(contactId: string): void {
form.recipient.email = contact.email
}
function markDocumentDatesEdited(): void {
documentDatesEdited.value = true
}
function applyDefaultDocumentDates(timeZone: string): void {
const dates = createDefaultDocumentDates(timeZone)
form.document.invoice_date = dates.invoiceDate
form.document.booking_date = dates.bookingDate
form.document.due_date = dates.dueDate
}
function triggerBrowserDownload(objectUrl: string, fileName: string): void {
const anchor = document.createElement('a')
anchor.href = objectUrl
anchor.download = fileName
anchor.rel = 'noreferrer'
anchor.style.display = 'none'
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
}
function createPdfFileName(invoiceGenerationId: string): string {
const safeId = invoiceGenerationId.replace(/[^a-zA-Z0-9_.-]/g, '-')
return `proforma-invoice-${safeId}.pdf`
}
function numberValue(value: string | number): number {
return parseNumericInput(value) ?? 0
}
@@ -1111,7 +1221,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
.primary-button:disabled,
.secondary-button:disabled,
.icon-button:disabled {
.icon-button:disabled,
.secondary-link:disabled {
cursor: not-allowed;
opacity: 0.5;
}
@@ -1175,6 +1286,24 @@ function isRecord(value: unknown): value is Record<string, unknown> {
padding-left: 18px;
}
.technical-details {
border-top: 1px solid rgb(190 18 60 / 16%);
margin-top: 12px;
padding-top: 12px;
color: var(--th-color-slate-500);
font-size: 12px;
}
.technical-details summary {
cursor: pointer;
color: #9f1239;
font-weight: 800;
}
.technical-details li {
word-break: break-word;
}
.result-panel h3 {
margin: 18px 0 12px;
color: var(--th-color-navy-950);
@@ -1196,7 +1325,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
.secondary-link {
border: 1px solid var(--th-color-border);
background: var(--th-color-white);
color: var(--th-color-blue-600);
cursor: pointer;
font: inherit;
padding: 10px 12px;
}