Files
igny8/frontend/src/pages/account/PlansAndBillingPage.tsx
IGNY8 VPS (Salman) 74a3441ee4 SEction 4 completeed
2025-12-27 02:59:27 +00:00

905 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Plans & Billing Page - Subscription & Payment Management
* Tabs: Current Plan, Upgrade Plan, Billing History
*
* Note: Usage tracking is consolidated in UsageAnalyticsPage (/account/usage)
*/
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import {
CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle,
Loader2, AlertCircle, CheckCircle, Download, Zap, Globe, Users, X
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { PricingPlan } from '../../components/ui/pricing-table';
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel';
// import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; // Hidden from regular users
// import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; // Moved to UsageAnalyticsPage
import { convertToPricingPlan } from '../../utils/pricingHelpers';
import {
getCreditBalance,
getCreditPackages,
getInvoices,
getAvailablePaymentMethods,
purchaseCreditPackage,
downloadInvoicePDF,
getPayments,
submitManualPayment,
createPaymentMethod,
deletePaymentMethod,
setDefaultPaymentMethod,
type CreditBalance,
type CreditPackage,
type Invoice,
type PaymentMethod,
type Payment,
getPlans,
getSubscriptions,
createSubscription,
cancelSubscription,
type Plan,
type Subscription,
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
type TabType = 'plan' | 'upgrade' | 'invoices';
export default function PlansAndBillingPage() {
const [activeTab, setActiveTab] = useState<TabType>('plan');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [planLoadingId, setPlanLoadingId] = useState<number | null>(null);
const [purchaseLoadingId, setPurchaseLoadingId] = useState<number | null>(null);
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
// Data states
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
const [packages, setPackages] = useState<CreditPackage[]>([]);
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [payments, setPayments] = useState<Payment[]>([]);
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
const [plans, setPlans] = useState<Plan[]>([]);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string | undefined>(undefined);
const [manualPayment, setManualPayment] = useState({
invoice_id: '',
amount: '',
payment_method: '',
reference: '',
notes: '',
});
const [newPaymentMethod, setNewPaymentMethod] = useState({
type: 'bank_transfer',
display_name: '',
instructions: '',
});
const { user } = useAuthStore.getState();
const hasLoaded = useRef(false);
const isAwsAdmin = user?.account?.slug === 'aws-admin';
const handleBillingError = (err: any, fallback: string) => {
const message = err?.message || fallback;
setError(message);
toast?.error?.(message);
};
const toast = useToast();
useEffect(() => {
if (hasLoaded.current) return;
hasLoaded.current = true;
loadData();
}, []);
const loadData = async (allowRetry = true) => {
try {
setLoading(true);
// Fetch in controlled sequence to avoid burst 429s on auth/system scopes
const balanceData = await getCreditBalance();
// Small gap between auth endpoints to satisfy tight throttles
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
const packagesPromise = getCreditPackages();
const invoicesPromise = getInvoices({});
const paymentsPromise = getPayments({});
const methodsPromise = getAvailablePaymentMethods();
const plansData = await getPlans();
await wait(400);
// Subscriptions: retry once on 429 after short backoff; do not hard-fail page
let subsData: { results: Subscription[] } = { results: [] };
try {
subsData = await getSubscriptions();
} catch (subErr: any) {
if (subErr?.status === 429 && allowRetry) {
await wait(2500);
try {
subsData = await getSubscriptions();
} catch {
subsData = { results: [] };
}
} else {
subsData = { results: [] };
}
}
const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([
packagesPromise,
invoicesPromise,
paymentsPromise,
methodsPromise,
]);
setCreditBalance(balanceData);
setPackages(packagesData.results || []);
setInvoices(invoicesData.results || []);
setPayments(paymentsData.results || []);
// Prefer manual payment method id 14 as default (tenant-facing)
const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false);
setPaymentMethods(methods);
if (methods.length > 0) {
// Preferred ordering: bank_transfer (default), then manual
const bank = methods.find((m) => m.type === 'bank_transfer');
const manual = methods.find((m) => m.type === 'manual');
const selected =
bank ||
manual ||
methods.find((m) => m.is_default) ||
methods[0];
setSelectedPaymentMethod((prev) => prev || selected.type || selected.id);
}
// Surface all active plans (avoid hiding plans and showing empty state)
const activePlans = (plansData.results || []).filter((p) => p.is_active !== false);
// Exclude Enterprise plan for non aws-admin accounts
const filteredPlans = activePlans.filter((p) => {
const name = (p.name || '').toLowerCase();
const slug = (p.slug || '').toLowerCase();
const isEnterprise = name.includes('enterprise') || slug === 'enterprise';
return isAwsAdmin ? true : !isEnterprise;
});
// Ensure the user's assigned plan is included even if subscriptions list is empty
const accountPlan = user?.account?.plan;
const isAccountEnterprise = (() => {
if (!accountPlan) return false;
const name = (accountPlan.name || '').toLowerCase();
const slug = (accountPlan.slug || '').toLowerCase();
return name.includes('enterprise') || slug === 'enterprise';
})();
const shouldIncludeAccountPlan = accountPlan && (!isAccountEnterprise || isAwsAdmin);
if (shouldIncludeAccountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) {
filteredPlans.push(accountPlan as any);
}
setPlans(filteredPlans);
const subs = subsData.results || [];
if (subs.length === 0 && shouldIncludeAccountPlan && accountPlan) {
subs.push({
id: accountPlan.id || 0,
plan: accountPlan,
status: 'active',
} as any);
}
setSubscriptions(subs);
} catch (err: any) {
// Handle throttling gracefully: don't block the page on subscriptions throttle
if (err?.status === 429 && allowRetry) {
setError('Request was throttled. Retrying...');
setTimeout(() => loadData(false), 2500);
} else if (err?.status === 429) {
setError(''); // suppress lingering banner
} else {
setError(err.message || 'Failed to load billing data');
console.error('Billing load error:', err);
}
} finally {
setLoading(false);
}
};
const handleSelectPlan = async (planId: number) => {
try {
if (!selectedPaymentMethod && paymentMethods.length > 0) {
setError('Select a payment method to continue');
return;
}
setPlanLoadingId(planId);
await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
toast?.success?.('Subscription updated');
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to update subscription');
} finally {
setPlanLoadingId(null);
}
};
const handleCancelSubscription = async () => {
if (!currentSubscription?.id) {
setError('No active subscription to cancel');
return;
}
try {
setPlanLoadingId(currentSubscription.id);
await cancelSubscription(currentSubscription.id);
toast?.success?.('Subscription cancellation requested');
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to cancel subscription');
} finally {
setPlanLoadingId(null);
}
};
const handlePurchase = async (packageId: number) => {
try {
if (!selectedPaymentMethod && paymentMethods.length > 0) {
setError('Select a payment method to continue');
return;
}
setPurchaseLoadingId(packageId);
await purchaseCreditPackage({
package_id: packageId,
payment_method: (selectedPaymentMethod as any) || 'stripe',
});
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to purchase credits');
} finally {
setPurchaseLoadingId(null);
setLoading(false);
}
};
const handleDownloadInvoice = async (invoiceId: number) => {
try {
const blob = await downloadInvoicePDF(invoiceId);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `invoice-${invoiceId}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err: any) {
handleBillingError(err, 'Failed to download invoice');
}
};
const handleSubmitManualPayment = async () => {
try {
const payload = {
invoice_id: manualPayment.invoice_id ? Number(manualPayment.invoice_id) : undefined,
amount: manualPayment.amount,
payment_method: manualPayment.payment_method || (selectedPaymentMethod as any) || 'manual',
reference: manualPayment.reference,
notes: manualPayment.notes,
};
await submitManualPayment(payload as any);
toast?.success?.('Manual payment submitted');
setManualPayment({ invoice_id: '', amount: '', payment_method: '', reference: '', notes: '' });
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to submit payment');
}
};
const handleAddPaymentMethod = async () => {
if (!newPaymentMethod.display_name.trim()) {
setError('Payment method name is required');
return;
}
try {
await createPaymentMethod(newPaymentMethod as any);
toast?.success?.('Payment method added');
setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' });
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to add payment method');
}
};
const handleRemovePaymentMethod = async (id: string) => {
try {
await deletePaymentMethod(id);
toast?.success?.('Payment method removed');
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to remove payment method');
}
};
const handleSetDefaultPaymentMethod = async (id: string) => {
try {
await setDefaultPaymentMethod(id);
toast?.success?.('Default payment method updated');
await loadData();
} catch (err: any) {
handleBillingError(err, 'Failed to set default');
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
</div>
);
}
const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0];
const currentPlanId = typeof currentSubscription?.plan === 'object' ? currentSubscription.plan.id : currentSubscription?.plan;
// Fallback to account plan if subscription is missing
const accountPlanId = user?.account?.plan?.id;
const effectivePlanId = currentPlanId || accountPlanId;
const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan;
const hasActivePlan = Boolean(effectivePlanId);
const hasPaymentMethods = paymentMethods.length > 0;
const subscriptionStatus = currentSubscription?.status || (hasActivePlan ? 'active' : 'none');
const hasPendingManualPayment = payments.some((p) => p.status === 'pending_approval');
const tabs = [
{ id: 'plan' as TabType, label: 'Current Plan', icon: <Package className="w-4 h-4" /> },
{ id: 'upgrade' as TabType, label: 'Upgrade Plan', icon: <Wallet className="w-4 h-4" /> },
{ id: 'invoices' as TabType, label: 'History', icon: <FileText className="w-4 h-4" /> },
];
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Your Subscription</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your plan and view usage
</p>
</div>
{/* Activation / pending payment notice */}
{!hasActivePlan && (
<div className="mb-4 p-4 rounded-lg border border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
No active plan. Choose a plan below to activate your account.
</div>
)}
{hasPendingManualPayment && (
<div className="mb-4 p-4 rounded-lg border border-[var(--color-info-200)] bg-[var(--color-info-50)] text-[var(--color-info-800)] dark:border-[var(--color-info-800)] dark:bg-[var(--color-info-900)]/20 dark:text-[var(--color-info-100)]">
We received your manual payment. Its pending admin approval; activation will complete once approved.
</div>
)}
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-600" />
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8 overflow-x-auto">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap
${activeTab === tab.id
? 'border-[var(--color-brand-500)] text-[var(--color-brand-500)]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="mt-6">
{/* Current Plan Tab */}
{activeTab === 'plan' && (
<div className="space-y-6">
{/* Current Plan Overview */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Plan Card */}
<Card className="p-6 lg:col-span-2">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Your Current Plan</h2>
{!hasActivePlan && (
<div className="p-4 mb-4 rounded-lg border border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200 flex items-start gap-3">
<AlertCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium">No Active Plan</p>
<p className="text-sm mt-1">Choose a plan below to activate your account and unlock all features.</p>
</div>
</div>
)}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.name || 'No Plan Selected'}
</div>
<div className="text-gray-600 dark:text-gray-400 mt-1">
{currentPlan?.description || 'Select a plan to unlock full access.'}
</div>
</div>
<Badge variant="soft" tone={hasActivePlan ? 'success' : 'warning'} className="text-sm px-3 py-1">
{hasActivePlan ? subscriptionStatus : 'Inactive'}
</Badge>
</div>
{/* Quick Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div className="p-4 bg-gradient-to-br from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/10 rounded-lg border border-brand-200 dark:border-brand-700">
<div className="flex items-center gap-2 text-sm text-brand-700 dark:text-brand-300 mb-1">
<Zap className="w-4 h-4" />
Monthly Credits
</div>
<div className="text-2xl font-bold text-brand-600 dark:text-brand-400">
{creditBalance?.plan_credits_per_month?.toLocaleString?.() || 0}
</div>
</div>
<div className="p-4 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/20 dark:to-success-800/10 rounded-lg border border-success-200 dark:border-success-700">
<div className="flex items-center gap-2 text-sm text-success-700 dark:text-success-300 mb-1">
<Wallet className="w-4 h-4" />
Current Balance
</div>
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
{creditBalance?.credits?.toLocaleString?.() || 0}
</div>
</div>
<div className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/10 rounded-lg border border-purple-200 dark:border-purple-700">
<div className="flex items-center gap-2 text-sm text-purple-700 dark:text-purple-300 mb-1">
<Package className="w-4 h-4" />
Renewal Date
</div>
<div className="text-lg font-bold text-purple-600 dark:text-purple-400">
{currentSubscription?.current_period_end
? new Date(currentSubscription.current_period_end).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
: '—'}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 flex gap-3">
<Button
variant="primary"
tone="brand"
onClick={() => setActiveTab('upgrade')}
startIcon={<ArrowUpCircle className="w-4 h-4" />}
>
Upgrade Plan
</Button>
<Button
variant="outline"
tone="neutral"
as={Link}
to="/account/usage"
>
View Usage
</Button>
{hasActivePlan && (
<Button
variant="outline"
tone="neutral"
disabled={planLoadingId === currentSubscription?.id}
onClick={() => setShowCancelConfirm(true)}
>
Cancel Plan
</Button>
)}
</div>
</div>
</Card>
{/* Plan Features Card */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Included Features</h2>
<div className="space-y-2">
{(currentPlan?.features && currentPlan.features.length > 0
? currentPlan.features
: ['AI Content Writer', 'Image Generation', 'Auto Publishing', 'Custom Prompts', 'Email Support', 'API Access'])
.map((feature: string, index: number) => (
<div key={index} className="flex items-start gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-success-600 dark:text-success-400 mt-0.5 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{feature}</span>
</div>
))}
</div>
</Card>
</div>
{/* Plan Limits Overview */}
{hasActivePlan && (
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Quick Limits Overview</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Key plan limits at a glance
</p>
</div>
<Button
variant="outline"
tone="brand"
size="sm"
as={Link}
to="/account/usage"
>
View All Usage
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2">
<Globe className="w-4 h-4" />
Sites
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.max_sites === 9999 ? '∞' : currentPlan?.max_sites || 0}
</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2">
<Users className="w-4 h-4" />
Team Members
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.max_users === 9999 ? '∞' : currentPlan?.max_users || 0}
</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2">
<FileText className="w-4 h-4" />
Content Words/mo
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.max_content_words === 9999999
? '∞'
: currentPlan?.max_content_words
? `${(currentPlan.max_content_words / 1000).toFixed(0)}K`
: 0}
</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2">
<Zap className="w-4 h-4" />
Monthly Credits
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.included_credits?.toLocaleString?.() || 0}
</div>
</div>
</div>
</Card>
)}
</div>
)}
{/* Purchase/Upgrade Tab */}
{activeTab === 'upgrade' && (
<div className="space-y-6">
{/* Upgrade Plans Section */}
<div>
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
{hasActivePlan ? 'Upgrade or Change Your Plan' : 'Choose Your Plan'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Select the plan that best fits your needs
</p>
</div>
<div className="mx-auto" style={{ maxWidth: '1560px' }}>
<PricingTable1
title=""
plans={plans
.filter(plan => {
// Only show paid plans (exclude Free Plan)
const planName = (plan.name || '').toLowerCase();
const planPrice = plan.price || 0;
return planPrice > 0 && !planName.includes('free');
})
.map(plan => ({
...convertToPricingPlan(plan),
buttonText: plan.id === currentPlanId ? 'Current Plan' : 'Choose Plan',
disabled: plan.id === currentPlanId || planLoadingId === plan.id,
}))}
showToggle={true}
onPlanSelect={(plan) => plan.id && handleSelectPlan(plan.id)}
/>
</div>
{/* Plan Change Policy */}
<Card className="p-6 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 mt-6">
<h3 className="font-semibold text-brand-900 dark:text-brand-100 mb-2 flex items-center gap-2">
<AlertCircle className="w-5 h-5" />
Plan Change Policy
</h3>
<ul className="space-y-2 text-sm text-brand-800 dark:text-brand-200">
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
Upgrades take effect immediately with prorated billing
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
Downgrades take effect at the end of your current billing period
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
Unused credits carry over when changing plans
</li>
<li className="flex items-start gap-2">
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
Cancel anytime - no long-term commitments
</li>
</ul>
</Card>
</div>
{/* Purchase Additional Credits Section - Hidden from regular users - removed for simplification */}
</div>
)}
{/* Billing History Tab */}
{activeTab === 'invoices' && (
<div className="space-y-6">
{/* Invoices Section */}
<Card className="overflow-hidden">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold">Invoices</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Invoice
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{invoices.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-400" />
No invoices yet
</td>
</tr>
) : (
invoices.map((invoice) => (
<tr key={invoice.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{invoice.invoice_number}</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
{new Date(invoice.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 font-semibold">${invoice.total_amount}</td>
<td className="px-6 py-4">
<Badge
variant="light"
color={invoice.status === 'paid' ? 'success' : 'warning'}
>
{invoice.status}
</Badge>
</td>
<td className="px-6 py-4 text-right">
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<Download className="w-4 h-4" />}
className="ml-auto"
onClick={() => handleDownloadInvoice(invoice.id)}
>
Download
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
{/* Payments Section */}
<Card className="overflow-hidden">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold">Payments</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Recent payments and manual submissions</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Method</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{payments.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
No payments yet
</td>
</tr>
) : (
payments.map((payment) => (
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
{payment.invoice_number || payment.invoice_id || '-'}
</td>
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white">
${payment.amount}
</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
{payment.payment_method}
</td>
<td className="px-6 py-4">
<Badge
variant="light"
color={
payment.status === 'succeeded' || payment.status === 'completed'
? 'success'
: payment.status === 'pending' || payment.status === 'processing'
? 'warning'
: 'error'
}
>
{payment.status}
</Badge>
</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
{new Date(payment.created_at).toLocaleDateString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
{/* Payment Methods Section */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">Payment Methods</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">Manage your payment methods</p>
</div>
</div>
<div className="space-y-4">
{paymentMethods.map((method) => (
<div key={method.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<CreditCard className="w-8 h-8 text-gray-400" />
<div>
<div className="font-medium text-gray-900 dark:text-white">{method.display_name}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">{method.type}</div>
{method.instructions && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">{method.instructions}</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
{method.is_enabled && (
<Badge variant="light" color="success">Active</Badge>
)}
{method.is_default ? (
<Badge variant="light" color="info">Default</Badge>
) : (
<Button variant="outline" size="sm" onClick={() => handleSetDefaultPaymentMethod(method.id)}>
Make Default
</Button>
)}
<Button variant="outline" size="sm" tone="neutral" onClick={() => handleRemovePaymentMethod(method.id)}>
Remove
</Button>
</div>
</div>
))}
{paymentMethods.length === 0 && (
<div className="text-center py-12 text-gray-500">
No payment methods configured
</div>
)}
</div>
</Card>
</div>
)}
</div>
{/* Cancellation Confirmation Modal */}
{showCancelConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-md">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Cancel Subscription</h2>
<button
onClick={() => setShowCancelConfirm(false)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="p-6 space-y-4">
<div className="flex items-start gap-3 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-amber-800 dark:text-amber-200">
<p className="font-medium mb-1">Are you sure you want to cancel?</p>
<p>Your subscription will remain active until the end of your current billing period. After that:</p>
</div>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2 pl-2">
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1"></span>
<span>You'll lose access to premium features</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">•</span>
<span>Remaining credits will be preserved for 30 days</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">•</span>
<span>You can resubscribe anytime to restore access</span>
</li>
</ul>
</div>
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-xl">
<Button
variant="outline"
tone="neutral"
onClick={() => setShowCancelConfirm(false)}
>
Keep Subscription
</Button>
<Button
variant="solid"
tone="danger"
onClick={async () => {
setShowCancelConfirm(false);
await handleCancelSubscription();
}}
disabled={planLoadingId === currentSubscription?.id}
>
{planLoadingId === currentSubscription?.id ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Cancelling...
</>
) : (
'Yes, Cancel Subscription'
)}
</Button>
</div>
</div>
</div>
)}
</div>
);
}