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)
|
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>
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user