class BillingDashboard extends HTMLElement {
constructor() {
super();
this.currentUsage = null;
this.subscription = null;
this.pricing = null;
this.invoices = [];
this.boundHandleClick = this.handleClick.bind(this);
this.loading = true;
this.error = null;
this.stripe = null;
}
async connectedCallback() {
this.addEventListener('click', this.boundHandleClick);
this.render();
await this.loadData();
this.render();
this.attachEventListeners();
await this.initStripe();
}
disconnectedCallback() {
this.removeEventListener('click', this.boundHandleClick);
}
async loadData() {
this.loading = true;
this.error = null;
try {
const [usage, subscription, pricing, invoices] = await Promise.all([
this.fetchCurrentUsage(),
this.fetchSubscription(),
this.fetchPricing(),
this.fetchInvoices()
]);
this.currentUsage = usage;
this.subscription = subscription;
this.pricing = pricing;
this.invoices = invoices;
this.loading = false;
} catch (error) {
console.error('Failed to load billing data:', error);
this.error = error.message || 'Failed to load billing data';
this.loading = false;
}
}
async initStripe() {
if (window.Stripe) {
const response = await fetch('/api/billing/stripe-key');
if (response.ok) {
const data = await response.json();
this.stripe = window.Stripe(data.publishable_key);
}
}
}
async fetchCurrentUsage() {
const response = await fetch('/api/billing/usage/current', {
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async fetchSubscription() {
const response = await fetch('/api/billing/subscription', {
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async fetchPricing() {
const response = await fetch('/api/billing/pricing');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async fetchInvoices() {
const response = await fetch('/api/billing/invoices?limit=10', {
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 4
}).format(amount);
}
formatGB(gb) {
if (gb >= 1024) {
return `${(gb / 1024).toFixed(2)} TB`;
}
return `${gb.toFixed(2)} GB`;
}
calculateEstimatedCost() {
if (!this.currentUsage || !this.pricing) return 0;
const storagePrice = parseFloat(this.pricing.storage_per_gb_month?.value || 0);
const bandwidthPrice = parseFloat(this.pricing.bandwidth_egress_per_gb?.value || 0);
const freeStorage = parseFloat(this.pricing.free_tier_storage_gb?.value || 0);
const freeBandwidth = parseFloat(this.pricing.free_tier_bandwidth_gb?.value || 0);
const storageGB = this.currentUsage.storage_gb;
const bandwidthGB = this.currentUsage.bandwidth_down_gb_today * 30;
const billableStorage = Math.max(0, Math.ceil(storageGB - freeStorage));
const billableBandwidth = Math.max(0, Math.ceil(bandwidthGB - freeBandwidth));
return (billableStorage * storagePrice) + (billableBandwidth * bandwidthPrice);
}
render() {
if (this.loading) {
this.innerHTML = '<div class="billing-dashboard"><div class="loading">Loading billing data...</div></div>';
return;
}
if (this.error) {
this.innerHTML = `<div class="billing-dashboard"><div class="error-message">Error: ${this.error}</div></div>`;
return;
}
const estimatedCost = this.calculateEstimatedCost();
const storageUsed = this.currentUsage?.storage_gb || 0;
const freeStorage = parseFloat(this.pricing?.free_tier_storage_gb?.value || 15);
const storagePercentage = Math.min(100, (storageUsed / freeStorage) * 100);
this.innerHTML = `
<div class="billing-dashboard">
<div class="billing-header">
<h2>Billing & Usage</h2>
<div class="subscription-badge ${this.subscription?.status === 'active' ? 'active' : 'inactive'}">
${this.subscription?.billing_type === 'pay_as_you_go' ? 'Pay As You Go' : this.subscription?.plan_name || 'Free'}
</div>
</div>
<div class="billing-cards">
<div class="billing-card usage-card">
<h3>Current Usage</h3>
<div class="usage-details">
<div class="usage-item">
<span class="usage-label">Storage</span>
<span class="usage-value">${this.formatGB(storageUsed)}</span>
</div>
<div class="usage-progress">
<div class="usage-progress-bar" style="width: ${storagePercentage}%"></div>
</div>
<div class="usage-info">${this.formatGB(freeStorage)} included free</div>
<div class="usage-item">
<span class="usage-label">Bandwidth (Today)</span>
<span class="usage-value">${this.formatGB(this.currentUsage?.bandwidth_down_gb_today || 0)}</span>
</div>
</div>
</div>
<div class="billing-card cost-card">
<h3>Estimated Monthly Cost</h3>
<div class="estimated-cost">${this.formatCurrency(estimatedCost)}</div>
<div class="cost-breakdown">
<div class="cost-item">
<span>Storage</span>
<span>${this.formatCurrency(Math.max(0, Math.ceil(storageUsed - freeStorage)) * parseFloat(this.pricing?.storage_per_gb_month?.value || 0))}</span>
</div>
<div class="cost-item">
<span>Bandwidth</span>
<span>${this.formatCurrency(0)}</span>
</div>
</div>
</div>
<div class="billing-card pricing-card">
<h3>Current Pricing</h3>
<div class="pricing-details">
<div class="pricing-item">
<span>Storage</span>
<span>${this.formatCurrency(parseFloat(this.pricing?.storage_per_gb_month?.value || 0))}/GB/month</span>
</div>
<div class="pricing-item">
<span>Bandwidth</span>
<span>${this.formatCurrency(parseFloat(this.pricing?.bandwidth_egress_per_gb?.value || 0))}/GB</span>
</div>
<div class="pricing-item">
<span>Free Tier</span>
<span>${this.formatGB(freeStorage)} storage, ${this.formatGB(parseFloat(this.pricing?.free_tier_bandwidth_gb?.value || 15))} bandwidth/month</span>
</div>
</div>
</div>
</div>
<div class="invoices-section">
<h3>Recent Invoices</h3>
<div class="invoices-table">
${this.renderInvoicesTable()}
</div>
</div>
<div class="payment-methods-section">
<h3>Payment Methods</h3>
<button class="button button-primary" id="addPaymentMethod">Add Payment Method</button>
</div>
</div>
`;
}
renderInvoicesTable() {
if (!this.invoices || this.invoices.length === 0) {
return '<p class="no-invoices">No invoices yet</p>';
}
return `
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Period</th>
<th>Amount</th>
<th>Status</th>
<th>Due Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${this.invoices.map(invoice => `
<tr>
<td>${invoice.invoice_number}</td>
<td>${this.formatDate(invoice.period_start)} - ${this.formatDate(invoice.period_end)}</td>
<td>${this.formatCurrency(invoice.total)}</td>
<td><span class="invoice-status ${invoice.status}">${invoice.status}</span></td>
<td>${invoice.due_date ? this.formatDate(invoice.due_date) : '-'}</td>
<td>
<button class="button" data-invoice-id="${invoice.id}">View</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
handleClick(e) {
const target = e.target;
if (target.id === 'addPaymentMethod') {
this.showPaymentMethodModal();
return;
}
if (target.dataset.invoiceId) {
const invoiceId = target.dataset.invoiceId;
this.showInvoiceDetail(invoiceId);
}
}
attachEventListeners() {
}
async showPaymentMethodModal() {
if (!this.stripe) {
alert('Payment processing not available');
return;
}
try {
const response = await fetch('/api/billing/payment-methods/setup-intent', {
method: 'POST',
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
});
if (!response.ok) {
const error = await response.json();
alert(`Failed to initialize payment: ${error.detail}`);
return;
}
const { client_secret } = await response.json();
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<h2>Add Payment Method</h2>
<div id="payment-element"></div>
<div class="modal-actions">
<button class="button button-primary" id="submitPayment">Add Card</button>
<button class="button" id="cancelPayment">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
const elements = this.stripe.elements({ clientSecret: client_secret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
modal.querySelector('#submitPayment').addEventListener('click', async () => {
const submitButton = modal.querySelector('#submitPayment');
submitButton.disabled = true;
submitButton.textContent = 'Processing...';
const { error } = await this.stripe.confirmSetup({
elements,
confirmParams: {
return_url: window.location.href,
},
redirect: 'if_required'
});
if (error) {
alert(`Payment failed: ${error.message}`);
submitButton.disabled = false;
submitButton.textContent = 'Add Card';
} else {
alert('Payment method added successfully');
modal.remove();
await this.loadData();
this.render();
}
});
modal.querySelector('#cancelPayment').addEventListener('click', () => {
modal.remove();
});
} catch (error) {
alert(`Error: ${error.message}`);
}
}
async showInvoiceDetail(invoiceId) {
const response = await fetch(`/api/billing/invoices/${invoiceId}`, {
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
});
const invoice = await response.json();
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<h2>Invoice ${invoice.invoice_number}</h2>
<div class="invoice-details">
<p><strong>Period:</strong> ${this.formatDate(invoice.period_start)} - ${this.formatDate(invoice.period_end)}</p>
<p><strong>Status:</strong> ${invoice.status}</p>
<h3>Line Items</h3>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
${invoice.line_items.map(item => `
<tr>
<td>${item.description}</td>
<td>${item.quantity.toFixed(2)}</td>
<td>${this.formatCurrency(item.unit_price)}</td>
<td>${this.formatCurrency(item.amount)}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="invoice-total">
<div><strong>Subtotal:</strong> ${this.formatCurrency(invoice.subtotal)}</div>
<div><strong>Tax:</strong> ${this.formatCurrency(invoice.tax)}</div>
<div><strong>Total:</strong> ${this.formatCurrency(invoice.total)}</div>
</div>
</div>
<button class="button" onclick="this.closest('.modal').remove()">Close</button>
</div>
`;
document.body.appendChild(modal);
}
}
customElements.define('billing-dashboard', BillingDashboard);
export default BillingDashboard;