|
class BillingDashboard extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.currentUsage = null;
|
|
this.subscription = null;
|
|
this.pricing = null;
|
|
this.invoices = [];
|
|
this.boundHandleClick = this.handleClick.bind(this);
|
|
}
|
|
|
|
async connectedCallback() {
|
|
this.addEventListener('click', this.boundHandleClick);
|
|
await this.loadData();
|
|
this.render();
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.removeEventListener('click', this.boundHandleClick);
|
|
}
|
|
|
|
async loadData() {
|
|
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;
|
|
} catch (error) {
|
|
console.error('Failed to load billing data:', error);
|
|
}
|
|
}
|
|
|
|
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() {
|
|
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="btn-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="btn-link" 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() {
|
|
alert('Payment method modal will be implemented with Stripe Elements');
|
|
}
|
|
|
|
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="btn-secondary" onclick="this.closest('.modal').remove()">Close</button>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
}
|
|
}
|
|
|
|
customElements.define('billing-dashboard', BillingDashboard);
|
|
|
|
export default BillingDashboard;
|