diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 4b90121..0881c88 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -16,18 +16,21 @@ import { OrganizationInvitation, Organization } from '@/lib/api/organization' -import { getSubscription, createPortalSession, SubscriptionDetails } from '@/lib/api/billing' +import { getSubscription, createPortalSession, getInvoices, SubscriptionDetails, Invoice } from '@/lib/api/billing' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { motion, AnimatePresence } from 'framer-motion' -import { - AlertTriangleIcon, - PlusIcon, - BoxIcon, - UserIcon, - CheckIcon, +import { + AlertTriangleIcon, + PlusIcon, + BoxIcon, + UserIcon, + CheckIcon, XIcon, - Captcha + Captcha, + BookOpenIcon, + DownloadIcon, + ExternalLinkIcon } from '@ciphera-net/ui' // @ts-ignore import { Button, Input } from '@ciphera-net/ui' @@ -50,6 +53,8 @@ export default function OrganizationSettings() { const [subscription, setSubscription] = useState(null) const [isLoadingSubscription, setIsLoadingSubscription] = useState(false) const [isRedirectingToPortal, setIsRedirectingToPortal] = useState(false) + const [invoices, setInvoices] = useState([]) + const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) // Invite State const [inviteEmail, setInviteEmail] = useState('') @@ -109,6 +114,19 @@ export default function OrganizationSettings() { } }, [currentOrgId]) + const loadInvoices = useCallback(async () => { + if (!currentOrgId) return + setIsLoadingInvoices(true) + try { + const invs = await getInvoices() + setInvoices(invs) + } catch (error) { + console.error('Failed to load invoices:', error) + } finally { + setIsLoadingInvoices(false) + } + }, [currentOrgId]) + useEffect(() => { if (currentOrgId) { loadMembers() @@ -120,8 +138,9 @@ export default function OrganizationSettings() { useEffect(() => { if (activeTab === 'billing' && currentOrgId) { loadSubscription() + loadInvoices() } - }, [activeTab, currentOrgId, loadSubscription]) + }, [activeTab, currentOrgId, loadSubscription, loadInvoices]) // If no org ID, we are in personal workspace, so don't show org settings if (!currentOrgId) { @@ -614,6 +633,74 @@ export default function OrganizationSettings() { )} + + {/* Invoice History */} +
+

Invoice History

+
+ {isLoadingInvoices ? ( +
+
+ Loading invoices... +
+ ) : invoices.length === 0 ? ( +
No invoices found.
+ ) : ( +
+ {invoices.map((invoice) => ( +
+
+
+ +
+
+
+ {(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })} +
+
+ {new Date(invoice.created * 1000).toLocaleDateString()} +
+
+
+
+ + {invoice.status} + + {invoice.invoice_pdf && ( + + + + )} + {invoice.hosted_invoice_url && ( + + + + )} +
+
+ ))} +
+ )} +
+
)} diff --git a/lib/api/billing.ts b/lib/api/billing.ts index 179591b..6d1e793 100644 --- a/lib/api/billing.ts +++ b/lib/api/billing.ts @@ -58,3 +58,20 @@ export async function createCheckoutSession(params: CreateCheckoutParams): Promi body: JSON.stringify(params), }) } + +export interface Invoice { + id: string + amount_paid: number + amount_due: number + currency: string + status: string + created: number + hosted_invoice_url: string + invoice_pdf: string +} + +export async function getInvoices(): Promise { + return await billingFetch('/api/billing/invoices', { + method: 'GET', + }) +}