From 6b291671bd103777665a528aa7462441ac2d2641 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Fri, 5 Dec 2025 00:11:06 +0000 Subject: [PATCH] billing adn account --- frontend/src/App.tsx | 32 +- frontend/src/layout/AppSidebar.tsx | 26 +- .../src/pages/account/AccountBillingPage.tsx | 350 ++++++++++++++ .../src/pages/account/PurchaseCreditsPage.tsx | 430 ++++++++++++++++++ .../src/pages/admin/PaymentApprovalPage.tsx | 300 ++++++++++++ frontend/src/services/billing.api.ts | 253 +++++++++++ 6 files changed, 1373 insertions(+), 18 deletions(-) create mode 100644 frontend/src/pages/account/AccountBillingPage.tsx create mode 100644 frontend/src/pages/account/PurchaseCreditsPage.tsx create mode 100644 frontend/src/pages/admin/PaymentApprovalPage.tsx create mode 100644 frontend/src/services/billing.api.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b597980e..e96c5ae7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -60,9 +60,12 @@ const Credits = lazy(() => import("./pages/Billing/Credits")); const Transactions = lazy(() => import("./pages/Billing/Transactions")); const Usage = lazy(() => import("./pages/Billing/Usage")); const CreditsAndBilling = lazy(() => import("./pages/Settings/CreditsAndBilling")); +const PurchaseCreditsPage = lazy(() => import("./pages/account/PurchaseCreditsPage")); +const AccountBillingPage = lazy(() => import("./pages/account/AccountBillingPage")); // Admin Module - Lazy loaded const AdminBilling = lazy(() => import("./pages/Admin/AdminBilling")); +const PaymentApprovalPage = lazy(() => import("./pages/admin/PaymentApprovalPage")); // Reference Data - Lazy loaded const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords")); @@ -334,32 +337,45 @@ export default function App() { - + } /> - + } /> - + } /> - + } /> - {/* Admin Routes */} + {/* Account Section - New Billing Pages */} + + + + } /> + + + + } /> {/* Admin Routes */} - + } /> - - {/* Reference Data */} + + + + } /> {/* Reference Data */} diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 623cb75b..3e9b338f 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -172,6 +172,21 @@ const AppSidebar: React.FC = () => { label: "WORKFLOW", items: workflowItems, }, + { + label: "ACCOUNT", + items: [ + { + icon: , + name: "Plans & Billing", + path: "/account/billing", + }, + { + icon: , + name: "Purchase Credits", + path: "/account/credits/purchase", + }, + ], + }, { label: "SETTINGS", items: [ @@ -186,16 +201,6 @@ const AppSidebar: React.FC = () => { { name: "Import / Export", path: "/settings/import-export" }, ], }, - { - icon: , - name: "Billing", - subItems: [ - { name: "Overview", path: "/billing/overview" }, - { name: "Credits", path: "/billing/credits" }, - { name: "Transactions", path: "/billing/transactions" }, - { name: "Usage", path: "/billing/usage" }, - ], - }, { icon: , name: "Help & Documentation", @@ -215,6 +220,7 @@ const AppSidebar: React.FC = () => { name: "Billing & Credits", subItems: [ { name: "Billing Management", path: "/admin/billing" }, + { name: "Payment Approvals", path: "/admin/payments/approvals" }, { name: "Credit Costs", path: "/admin/credit-costs" }, ], }, diff --git a/frontend/src/pages/account/AccountBillingPage.tsx b/frontend/src/pages/account/AccountBillingPage.tsx new file mode 100644 index 00000000..8620caaf --- /dev/null +++ b/frontend/src/pages/account/AccountBillingPage.tsx @@ -0,0 +1,350 @@ +/** + * Account Billing Page + * Consolidated billing dashboard with invoices, payments, and credit balance + */ + +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { + CreditCard, + Download, + AlertCircle, + Loader2, + FileText, + CheckCircle, + XCircle, + Clock, +} from 'lucide-react'; +import { + getInvoices, + getPayments, + getCreditBalance, + downloadInvoicePDF, + type Invoice, + type Payment, + type CreditBalance, +} from '../../services/billing.api'; + +type TabType = 'overview' | 'invoices' | 'payments'; + +export default function AccountBillingPage() { + const [activeTab, setActiveTab] = useState('overview'); + const [creditBalance, setCreditBalance] = useState(null); + const [invoices, setInvoices] = useState([]); + const [payments, setPayments] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + const [balanceRes, invoicesRes, paymentsRes] = await Promise.all([ + getCreditBalance(), + getInvoices(), + getPayments(), + ]); + + setCreditBalance(balanceRes); + setInvoices(invoicesRes.results); + setPayments(paymentsRes.results); + } catch (err: any) { + setError(err.message || 'Failed to load billing data'); + } finally { + setLoading(false); + } + }; + + const handleDownloadInvoice = async (invoiceId: number, invoiceNumber: string) => { + try { + const blob = await downloadInvoicePDF(invoiceId); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `invoice-${invoiceNumber}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (err) { + alert('Failed to download invoice'); + } + }; + + const getStatusBadge = (status: string) => { + const styles: Record = { + paid: { bg: 'bg-green-100', text: 'text-green-800', icon: CheckCircle }, + pending: { bg: 'bg-yellow-100', text: 'text-yellow-800', icon: Clock }, + failed: { bg: 'bg-red-100', text: 'text-red-800', icon: XCircle }, + void: { bg: 'bg-gray-100', text: 'text-gray-800', icon: XCircle }, + completed: { bg: 'bg-green-100', text: 'text-green-800', icon: CheckCircle }, + pending_approval: { bg: 'bg-blue-100', text: 'text-blue-800', icon: Clock }, + }; + + const style = styles[status] || styles.pending; + const Icon = style.icon; + + return ( + + + {status.replace('_', ' ').toUpperCase()} + + ); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+

Plans & Billing

+

Manage your subscription, credits, and billing

+
+ + + Purchase Credits + +
+ + {error && ( +
+ +

{error}

+
+ )} + + {/* Tabs */} +
+ +
+ + {/* Overview Tab */} + {activeTab === 'overview' && creditBalance && ( +
+ {/* Credit Balance Card */} +
+
+
+
Current Balance
+
+ {creditBalance.balance.toLocaleString()} +
+
credits
+
+ +
+
+ + {/* Plan Info */} +
+
+

Current Plan

+
+
+ Plan: + {creditBalance.subscription_plan} +
+
+ Monthly Credits: + + {creditBalance.monthly_credits.toLocaleString()} + +
+
+ Status: + + {getStatusBadge(creditBalance.subscription_status || 'active')} + +
+
+
+ +
+

Recent Activity

+
+
+
Total Invoices:
+
{invoices.length}
+
+
+
Paid Invoices:
+
+ {invoices.filter((i) => i.status === 'paid').length} +
+
+
+
Pending Payments:
+
+ {payments.filter((p) => p.status === 'pending_approval').length} +
+
+
+
+
+
+ )} + + {/* Invoices Tab */} + {activeTab === 'invoices' && ( +
+
+ + + + + + + + + + + + {invoices.length === 0 ? ( + + + + ) : ( + invoices.map((invoice) => ( + + + + + + + + )) + )} + +
+ Invoice + + Date + + Amount + + Status + + Actions +
+ + No invoices yet +
+
{invoice.invoice_number}
+ {invoice.line_items[0] && ( +
+ {invoice.line_items[0].description} +
+ )} +
+ {new Date(invoice.created_at).toLocaleDateString()} + + ${invoice.total_amount} + {getStatusBadge(invoice.status)} + +
+
+
+ )} + + {/* Payments Tab */} + {activeTab === 'payments' && ( +
+
+ + + + + + + + + + + + {payments.length === 0 ? ( + + + + ) : ( + payments.map((payment) => ( + + + + + + + + )) + )} + +
+ Date + + Method + + Reference + + Amount + + Status +
+ + No payments yet +
+ {new Date(payment.created_at).toLocaleDateString()} + +
+ {payment.payment_method.replace('_', ' ')} +
+
+ {payment.transaction_reference || '-'} + + ${payment.amount} + {getStatusBadge(payment.status)}
+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/account/PurchaseCreditsPage.tsx b/frontend/src/pages/account/PurchaseCreditsPage.tsx new file mode 100644 index 00000000..00ba4d12 --- /dev/null +++ b/frontend/src/pages/account/PurchaseCreditsPage.tsx @@ -0,0 +1,430 @@ +/** + * Purchase Credits Page + * Displays available credit packages and payment methods + */ + +import { useState, useEffect } from 'react'; +import { AlertCircle, Check, CreditCard, Building2, Wallet, Loader2 } from 'lucide-react'; +import { + getCreditPackages, + getAvailablePaymentMethods, + purchaseCreditPackage, + submitManualPayment, + type CreditPackage, + type PaymentMethod, +} from '../../services/billing.api'; + +export default function PurchaseCreditsPage() { + const [packages, setPackages] = useState([]); + const [paymentMethods, setPaymentMethods] = useState([]); + const [selectedPackage, setSelectedPackage] = useState(null); + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(''); + const [loading, setLoading] = useState(true); + const [purchasing, setPurchasing] = useState(false); + const [error, setError] = useState(''); + const [showManualPaymentForm, setShowManualPaymentForm] = useState(false); + const [manualPaymentData, setManualPaymentData] = useState({ + transaction_reference: '', + notes: '', + }); + const [invoiceData, setInvoiceData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + const [packagesRes, methodsRes] = await Promise.all([ + getCreditPackages(), + getAvailablePaymentMethods(), + ]); + + setPackages(packagesRes.results); + setPaymentMethods(methodsRes.methods); + + // Auto-select first payment method + if (methodsRes.methods.length > 0) { + setSelectedPaymentMethod(methodsRes.methods[0].type); + } + } catch (err) { + setError('Failed to load credit packages'); + console.error(err); + } finally { + setLoading(false); + } + }; + + const handlePurchase = async () => { + if (!selectedPackage || !selectedPaymentMethod) { + setError('Please select a package and payment method'); + return; + } + + try { + setPurchasing(true); + setError(''); + + const response = await purchaseCreditPackage( + selectedPackage.id, + selectedPaymentMethod + ); + + if (selectedPaymentMethod === 'stripe') { + // Redirect to Stripe checkout + setError('Stripe integration pending - coming soon!'); + } else if (selectedPaymentMethod === 'paypal') { + // Redirect to PayPal + setError('PayPal integration pending - coming soon!'); + } else { + // Manual payment - show form + setInvoiceData(response); + setShowManualPaymentForm(true); + } + } catch (err: any) { + setError(err.message || 'Failed to initiate purchase'); + } finally { + setPurchasing(false); + } + }; + + const handleManualPaymentSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!invoiceData || !manualPaymentData.transaction_reference) { + setError('Please provide transaction reference'); + return; + } + + try { + setPurchasing(true); + setError(''); + + await submitManualPayment({ + invoice_id: invoiceData.invoice_id, + payment_method: selectedPaymentMethod as 'bank_transfer' | 'local_wallet', + transaction_reference: manualPaymentData.transaction_reference, + notes: manualPaymentData.notes, + }); + + // Success + alert('Payment submitted successfully! Your payment will be reviewed within 1-2 business days.'); + setShowManualPaymentForm(false); + setSelectedPackage(null); + setManualPaymentData({ transaction_reference: '', notes: '' }); + setInvoiceData(null); + } catch (err: any) { + setError(err.message || 'Failed to submit payment'); + } finally { + setPurchasing(false); + } + }; + + const getPaymentMethodIcon = (type: string) => { + switch (type) { + case 'stripe': + return ; + case 'bank_transfer': + return ; + case 'local_wallet': + return ; + default: + return ; + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (showManualPaymentForm && invoiceData) { + const selectedMethod = paymentMethods.find((m) => m.type === selectedPaymentMethod); + + return ( +
+

Complete Payment

+ + {/* Invoice Details */} +
+

Invoice Details

+
+
+ Invoice Number: + {invoiceData.invoice_number} +
+
+ Package: + {selectedPackage?.name} +
+
+ Credits: + {selectedPackage?.credits.toLocaleString()} +
+
+ Total Amount: + ${invoiceData.total_amount} +
+
+
+ + {/* Payment Instructions */} +
+

Payment Instructions

+

{selectedMethod?.instructions}

+ + {selectedMethod?.bank_details && ( +
+

Bank Account Details:

+
+
Bank Name:
+
{selectedMethod.bank_details.bank_name}
+
Account Number:
+
{selectedMethod.bank_details.account_number}
+
Routing Number:
+
{selectedMethod.bank_details.routing_number}
+ {selectedMethod.bank_details.swift_code && ( + <> +
SWIFT Code:
+
{selectedMethod.bank_details.swift_code}
+ + )} +
+
+ )} + + {selectedMethod?.wallet_details && ( +
+

Wallet Details:

+
+
Wallet Type:
+
{selectedMethod.wallet_details.wallet_type}
+
Wallet ID:
+
{selectedMethod.wallet_details.wallet_id}
+
+
+ )} +
+ + {/* Manual Payment Form */} +
+

Submit Payment Proof

+ + {error && ( +
+ +

{error}

+
+ )} + +
+
+ + + setManualPaymentData({ ...manualPaymentData, transaction_reference: e.target.value }) + } + placeholder="Enter transaction ID or reference number" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ +
+ +