feat: add email report functionality to finance management page
This commit is contained in:
parent
ba28c54ae8
commit
7a8801274f
@ -18,6 +18,10 @@ export default function FinanceManagementPage() {
|
|||||||
const [diagData, setDiagData] = useState<any | null>(null)
|
const [diagData, setDiagData] = useState<any | null>(null)
|
||||||
const [selectedInvoice, setSelectedInvoice] = useState<AdminInvoice | null>(null)
|
const [selectedInvoice, setSelectedInvoice] = useState<AdminInvoice | null>(null)
|
||||||
const [detailModalOpen, setDetailModalOpen] = useState(false)
|
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
|
// NEW: fetch invoices from backend
|
||||||
const {
|
const {
|
||||||
@ -116,6 +120,39 @@ export default function FinanceManagementPage() {
|
|||||||
URL.revokeObjectURL(url)
|
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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
||||||
@ -182,6 +219,7 @@ export default function FinanceManagementPage() {
|
|||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2>
|
<h2 className="text-lg font-semibold text-[#1C2B4A]">Invoices</h2>
|
||||||
<div className="flex flex-wrap gap-2 text-sm">
|
<div className="flex flex-wrap gap-2 text-sm">
|
||||||
|
<button onClick={() => { setReportMsg(null); setEmailDialogOpen(true) }} className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-blue-900 font-medium hover:bg-blue-100">Send Email Report</button>
|
||||||
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button>
|
<button onClick={() => exportBills('csv')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export CSV</button>
|
||||||
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button>
|
<button onClick={() => exportBills('pdf')} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Export PDF</button>
|
||||||
<button onClick={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
|
<button onClick={reload} className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50">Reload</button>
|
||||||
@ -222,6 +260,11 @@ export default function FinanceManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
{reportMsg && (
|
||||||
|
<div className={`rounded-md border px-3 py-2 text-sm mb-3 ${reportMsg.type === 'success' ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-700'}`}>
|
||||||
|
{reportMsg.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{invError && (
|
{invError && (
|
||||||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
|
||||||
{invError}
|
{invError}
|
||||||
@ -366,6 +409,47 @@ export default function FinanceManagementPage() {
|
|||||||
onExport={(inv) => exportInvoice(inv)}
|
onExport={(inv) => exportInvoice(inv)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Email Report Dialog */}
|
||||||
|
{emailDialogOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||||
|
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">
|
||||||
|
<h3 className="text-lg font-semibold text-[#1C2B4A] mb-1">Send Email Report</h3>
|
||||||
|
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">
|
||||||
|
Only <strong>paid</strong> invoices will be included in the report, regardless of the status filter.
|
||||||
|
{(billFilter.from || billFilter.to) && (
|
||||||
|
<span> The current date range filter ({billFilter.from || '…'} – {billFilter.to || '…'}) will be applied.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Recipient Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={reportEmail}
|
||||||
|
onChange={e => 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() }}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setEmailDialogOpen(false); setReportEmail('') }}
|
||||||
|
disabled={sendingReport}
|
||||||
|
className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={sendEmailReport}
|
||||||
|
disabled={sendingReport || !reportEmail.trim()}
|
||||||
|
className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{sendingReport ? 'Sending…' : 'Send Report'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user