From 7a8801274fa63443361105995774ff754d218e76 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Tue, 7 Apr 2026 16:49:56 +0200 Subject: [PATCH 1/2] feat: add email report functionality to finance management page --- src/app/admin/finance-management/page.tsx | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 96d72ae..1d4db21 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -18,6 +18,10 @@ export default function FinanceManagementPage() { const [diagData, setDiagData] = useState(null) const [selectedInvoice, setSelectedInvoice] = useState(null) const [detailModalOpen, setDetailModalOpen] = useState(false) + const [emailDialogOpen, setEmailDialogOpen] = useState(false) + const [reportEmail, setReportEmail] = useState('') + const [sendingReport, setSendingReport] = useState(false) + const [reportMsg, setReportMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null) // NEW: fetch invoices from backend const { @@ -116,6 +120,39 @@ export default function FinanceManagementPage() { URL.revokeObjectURL(url) } + const sendEmailReport = async () => { + if (!reportEmail.trim()) return + setReportMsg(null) + setSendingReport(true) + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const res = await fetch(`${base}/api/admin/invoices/email-report`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({ + email: reportEmail.trim(), + from: billFilter.from || undefined, + to: billFilter.to || undefined, + }), + }) + const body = await res.json().catch(() => ({})) + if (!res.ok || body?.success === false) { + throw new Error(body?.message || `Request failed (${res.status})`) + } + setReportMsg({ type: 'success', text: `Report sent to ${reportEmail.trim()} (${body.data?.sentCount ?? 0} paid invoice(s)).` }) + setEmailDialogOpen(false) + setReportEmail('') + } catch (e: any) { + setReportMsg({ type: 'error', text: e?.message || 'Failed to send email report.' }) + } finally { + setSendingReport(false) + } + } + return (
@@ -182,6 +219,7 @@ export default function FinanceManagementPage() {

Invoices

+ @@ -222,6 +260,11 @@ export default function FinanceManagementPage() {
+ {reportMsg && ( +
+ {reportMsg.text} +
+ )} {invError && (
{invError} @@ -366,6 +409,47 @@ export default function FinanceManagementPage() { onExport={(inv) => exportInvoice(inv)} /> )} + + {/* Email Report Dialog */} + {emailDialogOpen && ( +
+
+

Send Email Report

+
+ Only paid invoices will be included in the report, regardless of the status filter. + {(billFilter.from || billFilter.to) && ( + The current date range filter ({billFilter.from || '…'} – {billFilter.to || '…'}) will be applied. + )} +
+ + setReportEmail(e.target.value)} + placeholder="email@example.com" + className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-900 focus:border-transparent" + autoFocus + onKeyDown={e => { if (e.key === 'Enter' && !sendingReport) sendEmailReport() }} + /> +
+ + +
+
+
+ )}
From 4ff36d172818455659f73f67bef2c8a89a0b2088 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Tue, 7 Apr 2026 17:04:05 +0200 Subject: [PATCH 2/2] feat: add PDF viewing functionality for invoices in finance management --- src/app/admin/finance-management/page.tsx | 36 ++++++++++++++++++- .../profile/components/financeInvoices.tsx | 21 ++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 1d4db21..9b559ba 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -120,6 +120,33 @@ export default function FinanceManagementPage() { URL.revokeObjectURL(url) } + const [pdfLoading, setPdfLoading] = useState(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() { + diff --git a/src/app/profile/components/financeInvoices.tsx b/src/app/profile/components/financeInvoices.tsx index f055ba7..22ca016 100644 --- a/src/app/profile/components/financeInvoices.tsx +++ b/src/app/profile/components/financeInvoices.tsx @@ -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(null) const [actionError, setActionError] = React.useState(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) => {