feat: add PDF viewing functionality for invoices in finance management

This commit is contained in:
seaznCode 2026-04-07 17:04:05 +02:00
parent 7a8801274f
commit 4ff36d1728
2 changed files with 52 additions and 5 deletions

View File

@ -120,6 +120,33 @@ export default function FinanceManagementPage() {
URL.revokeObjectURL(url) 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 () => { const sendEmailReport = async () => {
if (!reportEmail.trim()) return if (!reportEmail.trim()) return
setReportMsg(null) setReportMsg(null)
@ -385,11 +412,18 @@ export default function FinanceManagementPage() {
</span> </span>
</td> </td>
<td className="px-3 py-2 space-x-2"> <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 <button
onClick={() => { setSelectedInvoice(inv); setDetailModalOpen(true) }} onClick={() => { setSelectedInvoice(inv); setDetailModalOpen(true) }}
className="text-xs rounded border px-2 py-1 hover:bg-gray-50" className="text-xs rounded border px-2 py-1 hover:bg-gray-50"
> >
View Details
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -28,8 +28,10 @@ const isAbsUrl = (url: string) => /^https?:\/\//i.test(url)
const resolveInvoiceUrl = (invoice: AboInvoice) => { const resolveInvoiceUrl = (invoice: AboInvoice) => {
const raw = invoice.pdfUrl || invoice.downloadUrl || invoice.htmlUrl || invoice.fileUrl const raw = invoice.pdfUrl || invoice.downloadUrl || invoice.htmlUrl || invoice.fileUrl
if (!raw) return null if (raw) return isAbsUrl(raw) ? raw : `${BASE_URL}${raw.startsWith('/') ? '' : '/'}${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' 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 [busyId, setBusyId] = React.useState<string | number | null>(null)
const [actionError, setActionError] = React.useState<string | null>(null) const [actionError, setActionError] = React.useState<string | null>(null)
const onView = (invoice: AboInvoice) => { const onView = async (invoice: AboInvoice) => {
setActionError(null) setActionError(null)
const url = resolveInvoiceUrl(invoice) const url = resolveInvoiceUrl(invoice)
if (!url) { if (!url) {
setActionError('No view URL is available for this invoice.') setActionError('No view URL is available for this invoice.')
return 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) => { const onDownload = async (invoice: AboInvoice) => {