493 lines
24 KiB
TypeScript
493 lines
24 KiB
TypeScript
'use client'
|
||
import React, { useMemo, useState } from 'react'
|
||
import PageLayout from '../../components/PageLayout'
|
||
import { useRouter } from 'next/navigation'
|
||
import { useVatRates } from './hooks/getTaxes'
|
||
import { useAdminInvoices, type AdminInvoice } from './hooks/getInvoices'
|
||
import useAuthStore from '../../store/authStore'
|
||
import InvoiceDetailModal from './components/InvoiceDetailModal'
|
||
|
||
export default function FinanceManagementPage() {
|
||
const router = useRouter()
|
||
const accessToken = useAuthStore(s => s.accessToken)
|
||
const { rates, loading: vatLoading, error: vatError } = useVatRates()
|
||
const [timeframe, setTimeframe] = useState<'7d' | '30d' | '90d' | 'ytd'>('30d')
|
||
const [billFilter, setBillFilter] = useState({ query: '', status: 'all', from: '', to: '' })
|
||
const [diagLoading, setDiagLoading] = useState(false)
|
||
const [diagError, setDiagError] = useState('')
|
||
const [diagData, setDiagData] = useState<any | null>(null)
|
||
const [selectedInvoice, setSelectedInvoice] = useState<AdminInvoice | null>(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 {
|
||
invoices,
|
||
loading: invLoading,
|
||
error: invError,
|
||
reload,
|
||
} = useAdminInvoices({
|
||
status: billFilter.status !== 'all' ? billFilter.status : undefined,
|
||
limit: 200,
|
||
offset: 0,
|
||
})
|
||
|
||
// NEW: totals from backend invoices
|
||
const totals = useMemo(() => {
|
||
const now = new Date()
|
||
const inRange = (d: Date) => {
|
||
const diff = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)
|
||
if (timeframe === '7d') return diff <= 7
|
||
if (timeframe === '30d') return diff <= 30
|
||
if (timeframe === '90d') return diff <= 90
|
||
return true
|
||
}
|
||
const range = invoices.filter(inv => {
|
||
const dStr = inv.issued_at ?? inv.created_at
|
||
if (!dStr) return false
|
||
const d = new Date(dStr)
|
||
return inRange(d)
|
||
})
|
||
const totalAll = invoices.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0)
|
||
const totalRange = range.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0)
|
||
return { totalAll, totalRange }
|
||
}, [invoices, timeframe])
|
||
|
||
// NEW: filtered rows for table
|
||
const filteredBills = useMemo(() => {
|
||
const q = billFilter.query.trim().toLowerCase()
|
||
const from = billFilter.from ? new Date(billFilter.from) : null
|
||
const to = billFilter.to ? new Date(billFilter.to) : null
|
||
|
||
return invoices.filter(inv => {
|
||
const byQuery =
|
||
!q ||
|
||
String(inv.invoice_number ?? inv.id).toLowerCase().includes(q) ||
|
||
String(inv.buyer_name ?? '').toLowerCase().includes(q)
|
||
const issued = inv.issued_at ? new Date(inv.issued_at) : (inv.created_at ? new Date(inv.created_at) : null)
|
||
const byFrom = from ? (issued ? issued >= from : false) : true
|
||
const byTo = to ? (issued ? issued <= to : false) : true
|
||
return byQuery && byFrom && byTo
|
||
})
|
||
}, [invoices, billFilter])
|
||
|
||
const exportBills = (format: 'csv' | 'pdf') => {
|
||
console.log('[export]', format, { filters: billFilter, invoices: filteredBills })
|
||
alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} invoices`)
|
||
}
|
||
|
||
const runPoolCheck = async (invoiceId: string | number) => {
|
||
setDiagLoading(true)
|
||
setDiagError('')
|
||
setDiagData(null)
|
||
try {
|
||
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
|
||
const url = `${base}/api/admin/pools/inflow-diagnostics?invoiceId=${encodeURIComponent(String(invoiceId))}`
|
||
const res = await fetch(url, {
|
||
method: 'GET',
|
||
credentials: 'include',
|
||
headers: {
|
||
Accept: 'application/json',
|
||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||
},
|
||
})
|
||
const body = await res.json().catch(() => ({}))
|
||
if (!res.ok || body?.success === false) {
|
||
setDiagError(body?.message || `Check failed (${res.status})`)
|
||
return
|
||
}
|
||
setDiagData(body?.data || null)
|
||
} catch (e: any) {
|
||
setDiagError(e?.message || 'Network error')
|
||
} finally {
|
||
setDiagLoading(false)
|
||
}
|
||
}
|
||
|
||
const exportInvoice = (inv: AdminInvoice) => {
|
||
const pretty = JSON.stringify(inv, null, 2)
|
||
const blob = new Blob([pretty], { type: 'application/json;charset=utf-8' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `invoice-${inv.invoice_number || inv.id}.json`
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
a.remove()
|
||
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)
|
||
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 (
|
||
<PageLayout>
|
||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||
<header className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex flex-col gap-2">
|
||
<h1 className="text-3xl font-extrabold text-blue-900">Finance Management</h1>
|
||
<p className="text-sm text-blue-700">Overview of taxes, revenue, and invoices.</p>
|
||
</header>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
|
||
<div className="text-xs text-gray-500 mb-1">Total revenue (all time)</div>
|
||
<div className="text-2xl font-semibold text-[#1C2B4A]">€{totals.totalAll.toFixed(2)}</div>
|
||
</div>
|
||
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
|
||
<div className="text-xs text-gray-500 mb-1">Revenue (range)</div>
|
||
<div className="text-2xl font-semibold text-[#1C2B4A]">€{totals.totalRange.toFixed(2)}</div>
|
||
</div>
|
||
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
|
||
<div className="text-xs text-gray-500 mb-1">Invoices (range)</div>
|
||
<div className="text-2xl font-semibold text-[#1C2B4A]">{filteredBills.length}</div>
|
||
</div>
|
||
<div className="rounded-2xl border border-gray-100 bg-white shadow-lg p-5">
|
||
<div className="text-xs text-gray-500 mb-1">Timeframe</div>
|
||
<select
|
||
value={timeframe}
|
||
onChange={e => setTimeframe(e.target.value as any)}
|
||
className="mt-2 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||
>
|
||
<option value="7d">Last 7 days</option>
|
||
<option value="30d">Last 30 days</option>
|
||
<option value="90d">Last 90 days</option>
|
||
<option value="ytd">YTD</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* VAT summary */}
|
||
<section className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-[#1C2B4A]">Manage VAT rates</h2>
|
||
<p className="text-xs text-gray-600">Live data from backend; edit on a separate page.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => router.push('/admin/finance-management/vat-edit')}
|
||
className="rounded-lg bg-[#1C2B4A] px-4 py-2 text-sm font-semibold text-white shadow hover:bg-[#1C2B4A]/90"
|
||
>
|
||
Edit VAT
|
||
</button>
|
||
</div>
|
||
<div className="text-sm text-gray-700">
|
||
{vatLoading && 'Loading VAT rates...'}
|
||
{vatError && <span className="text-red-600">{vatError}</span>}
|
||
{!vatLoading && !vatError && (
|
||
<>Active countries: {rates.length} • Examples: {rates.slice(0, 5).map(r => r.country_code).join(', ')}</>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Bills list & filters */}
|
||
<section className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 space-y-4">
|
||
<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>
|
||
<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('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>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-3 md:grid-cols-4 text-sm">
|
||
<input
|
||
placeholder="Search (invoice no., customer)"
|
||
value={billFilter.query}
|
||
onChange={e => setBillFilter(f => ({ ...f, query: e.target.value }))}
|
||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||
/>
|
||
<select
|
||
value={billFilter.status}
|
||
onChange={e => setBillFilter(f => ({ ...f, status: e.target.value }))}
|
||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||
>
|
||
<option value="all">Status: All</option>
|
||
<option value="draft">Draft</option>
|
||
<option value="issued">Issued</option>
|
||
<option value="paid">Paid</option>
|
||
<option value="overdue">Overdue</option>
|
||
<option value="canceled">Canceled</option>
|
||
</select>
|
||
<input
|
||
type="date"
|
||
value={billFilter.from}
|
||
onChange={e => setBillFilter(f => ({ ...f, from: e.target.value }))}
|
||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||
/>
|
||
<input
|
||
type="date"
|
||
value={billFilter.to}
|
||
onChange={e => setBillFilter(f => ({ ...f, to: e.target.value }))}
|
||
className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
||
/>
|
||
</div>
|
||
|
||
<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 && (
|
||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 mb-3">
|
||
{invError}
|
||
</div>
|
||
)}
|
||
{(diagLoading || diagError || diagData) && (
|
||
<div className="rounded-md border border-blue-100 bg-blue-50/60 px-3 py-3 text-sm mb-3">
|
||
{diagLoading && <div className="text-blue-800">Checking pool inflow...</div>}
|
||
{!diagLoading && diagError && <div className="text-red-700">{diagError}</div>}
|
||
{!diagLoading && !diagError && diagData && (
|
||
<div className="space-y-2">
|
||
<div className="text-blue-900 font-semibold">Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}</div>
|
||
<div className="text-gray-700">
|
||
Status: <span className="font-medium">{diagData.ok ? 'OK' : 'Blocked'}</span> • Reason: <span className="font-mono">{diagData.reason}</span>
|
||
</div>
|
||
{diagData.ok && (
|
||
<div className="text-gray-700">
|
||
Abonement: <span className="font-medium">{diagData.abonement_id}</span> • Will book: <span className="font-medium">{diagData.will_book_count}</span> • Already booked: <span className="font-medium">{diagData.already_booked_count}</span>
|
||
</div>
|
||
)}
|
||
{Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && (
|
||
<div className="overflow-x-auto">
|
||
<table className="min-w-full text-xs">
|
||
<thead>
|
||
<tr className="text-left text-blue-900">
|
||
<th className="pr-3 py-1">Pool</th>
|
||
<th className="pr-3 py-1">Coffee</th>
|
||
<th className="pr-3 py-1">Capsules</th>
|
||
<th className="pr-3 py-1">Amount (gross)</th>
|
||
<th className="pr-3 py-1">Booked</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{diagData.candidates.map((c: any) => (
|
||
<tr key={`${c.pool_id}-${c.coffee_table_id}`}>
|
||
<td className="pr-3 py-1">{c.pool_name}</td>
|
||
<td className="pr-3 py-1">#{c.coffee_table_id}</td>
|
||
<td className="pr-3 py-1">{c.capsules_count}</td>
|
||
<td className="pr-3 py-1">€{Number(c.amount_gross ?? c.amount_net ?? 0).toFixed(2)}</td>
|
||
<td className="pr-3 py-1">{c.already_booked ? 'yes' : 'no'}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
<table className="min-w-full text-sm">
|
||
<thead>
|
||
<tr className="bg-blue-50 text-left text-blue-900">
|
||
<th className="px-3 py-2 font-semibold">Invoice</th>
|
||
<th className="px-3 py-2 font-semibold">Customer</th>
|
||
<th className="px-3 py-2 font-semibold">Issued</th>
|
||
<th className="px-3 py-2 font-semibold">Due Date</th>
|
||
<th className="px-3 py-2 font-semibold">Amount</th>
|
||
<th className="px-3 py-2 font-semibold">Status</th>
|
||
<th className="px-3 py-2 font-semibold">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{invLoading ? (
|
||
<>
|
||
<tr><td colSpan={7} className="px-3 py-3"><div className="h-4 w-40 bg-gray-200 animate-pulse rounded" /></td></tr>
|
||
<tr><td colSpan={7} className="px-3 py-3"><div className="h-4 w-3/4 bg-gray-200 animate-pulse rounded" /></td></tr>
|
||
</>
|
||
) : filteredBills.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={7} className="px-3 py-4 text-center text-gray-500">
|
||
Keine Rechnungen gefunden.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
filteredBills.map(inv => (
|
||
<tr key={inv.id} className="border-b last:border-0">
|
||
<td className="px-3 py-2">{inv.invoice_number ?? inv.id}</td>
|
||
<td className="px-3 py-2">{inv.buyer_name ?? '—'}</td>
|
||
<td className="px-3 py-2">{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'}</td>
|
||
<td className="px-3 py-2">
|
||
{(() => {
|
||
if (!inv.due_at) return <span className="text-gray-400">—</span>
|
||
const due = new Date(inv.due_at)
|
||
const now = new Date()
|
||
const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||
let cls = 'bg-green-100 text-green-700' // plenty of time
|
||
if (inv.status === 'paid') cls = 'bg-green-100 text-green-700'
|
||
else if (diffDays < 0) cls = 'bg-red-100 text-red-700'
|
||
else if (diffDays <= 3) cls = 'bg-red-100 text-red-700'
|
||
else if (diffDays <= 7) cls = 'bg-amber-100 text-amber-700'
|
||
return (
|
||
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold ${cls}`}>
|
||
{due.toLocaleDateString()}
|
||
</span>
|
||
)
|
||
})()}
|
||
</td>
|
||
<td className="px-3 py-2">
|
||
€{Number(inv.total_gross ?? 0).toFixed(2)}{' '}
|
||
<span className="text-xs text-gray-500">{inv.currency ?? 'EUR'}</span>
|
||
</td>
|
||
<td className="px-3 py-2">
|
||
<span
|
||
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||
inv.status === 'paid'
|
||
? 'bg-green-100 text-green-700'
|
||
: inv.status === 'issued'
|
||
? 'bg-indigo-100 text-indigo-700'
|
||
: inv.status === 'draft'
|
||
? 'bg-gray-100 text-gray-700'
|
||
: inv.status === 'overdue'
|
||
? 'bg-red-100 text-red-700'
|
||
: 'bg-yellow-100 text-yellow-700'
|
||
}`}
|
||
>
|
||
{inv.status}
|
||
</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"
|
||
>
|
||
Details
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{selectedInvoice && (
|
||
<InvoiceDetailModal
|
||
invoice={selectedInvoice}
|
||
open={detailModalOpen}
|
||
onClose={() => { setDetailModalOpen(false); setTimeout(() => setSelectedInvoice(null), 200) }}
|
||
onStatusChanged={reload}
|
||
onRunPoolCheck={(id) => { setDetailModalOpen(false); runPoolCheck(id) }}
|
||
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>
|
||
</div>
|
||
</div>
|
||
</PageLayout>
|
||
)
|
||
}
|