feat: add PDF viewing functionality for invoices in finance management
This commit is contained in:
parent
7a8801274f
commit
4ff36d1728
@ -120,6 +120,33 @@ export default function FinanceManagementPage() {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const [pdfLoading, setPdfLoading] = useState<string | number | null>(null)
|
||||
|
||||
const viewInvoicePdf = async (inv: AdminInvoice) => {
|
||||
setPdfLoading(inv.id)
|
||||
try {
|
||||
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
|
||||
const res = await fetch(`${base}/api/invoices/${inv.id}/pdf`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}))
|
||||
throw new Error(body?.message || `Failed to load PDF (${res.status})`)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
||||
} catch (e: any) {
|
||||
setReportMsg({ type: 'error', text: e?.message || 'Failed to load invoice PDF.' })
|
||||
} finally {
|
||||
setPdfLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const sendEmailReport = async () => {
|
||||
if (!reportEmail.trim()) return
|
||||
setReportMsg(null)
|
||||
@ -385,11 +412,18 @@ export default function FinanceManagementPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 space-x-2">
|
||||
<button
|
||||
onClick={() => viewInvoicePdf(inv)}
|
||||
disabled={pdfLoading === inv.id || !inv.pdf_storage_key}
|
||||
className="text-xs rounded border px-2 py-1 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{pdfLoading === inv.id ? 'Loading…' : 'View PDF'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setSelectedInvoice(inv); setDetailModalOpen(true) }}
|
||||
className="text-xs rounded border px-2 py-1 hover:bg-gray-50"
|
||||
>
|
||||
View
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -28,8 +28,10 @@ const isAbsUrl = (url: string) => /^https?:\/\//i.test(url)
|
||||
|
||||
const resolveInvoiceUrl = (invoice: AboInvoice) => {
|
||||
const raw = invoice.pdfUrl || invoice.downloadUrl || invoice.htmlUrl || invoice.fileUrl
|
||||
if (!raw) return null
|
||||
return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}`
|
||||
if (raw) return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${raw}`
|
||||
// Fallback: use the backend PDF proxy endpoint if an id is available
|
||||
if (invoice.id) return `${BASE_URL}/api/invoices/${invoice.id}/pdf`
|
||||
return null
|
||||
}
|
||||
|
||||
type UiLifecycleStatus = 'issued' | 'ongoing' | 'finished' | 'pause' | 'cancelled'
|
||||
@ -80,14 +82,25 @@ export default function FinanceInvoices({ abonementId }: Props) {
|
||||
const [busyId, setBusyId] = React.useState<string | number | null>(null)
|
||||
const [actionError, setActionError] = React.useState<string | null>(null)
|
||||
|
||||
const onView = (invoice: AboInvoice) => {
|
||||
const onView = async (invoice: AboInvoice) => {
|
||||
setActionError(null)
|
||||
const url = resolveInvoiceUrl(invoice)
|
||||
if (!url) {
|
||||
setActionError('No view URL is available for this invoice.')
|
||||
return
|
||||
}
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
setBusyId(invoice.id)
|
||||
try {
|
||||
const res = await authFetch(url, { method: 'GET' })
|
||||
if (!res.ok) throw new Error(`Failed to load PDF: ${res.status}`)
|
||||
const blob = await res.blob()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
||||
} catch (e: any) {
|
||||
setActionError(e?.message || 'Failed to load invoice PDF.')
|
||||
} finally {
|
||||
setBusyId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const onDownload = async (invoice: AboInvoice) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user