修复手工开票下载与错误提示
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user