a11oy / web /src /components /operations /BillingHealthCard.tsx
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame
5.93 kB
import { useBillingHealth, type BillingHealthSummary } from '@szl-holdings/billing-client';
import {
AlertTriangle,
CreditCard,
DollarSign,
FileText,
RefreshCw,
RotateCcw,
} from 'lucide-react';
import { Link } from 'wouter';
function isDemoMode(): boolean {
try {
const env = (import.meta as unknown as { env?: Record<string, unknown> }).env;
return env?.VITE_BILLING_DEMO_MODE === 'true' || env?.VITE_BILLING_DEMO_MODE === true;
} catch {
return false;
}
}
function fmtCurrency(cents: number, currency = 'usd'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase(),
maximumFractionDigits: 0,
}).format(cents / 100);
}
interface MetricChipProps {
icon: React.ElementType;
label: string;
value: string | number;
accent?: string;
alert?: boolean;
}
function MetricChip({ icon: Icon, label, value, accent = '#d4a054', alert = false }: MetricChipProps) {
return (
<div
className="flex flex-col gap-1 rounded-xl px-4 py-3 border"
style={{
background: alert ? 'rgba(239,68,68,0.05)' : 'rgba(255,255,255,0.02)',
borderColor: alert ? 'rgba(239,68,68,0.2)' : 'rgba(255,255,255,0.06)',
}}
>
<div className="flex items-center gap-1.5 mb-0.5">
<Icon className="w-3.5 h-3.5" style={{ color: alert ? '#ef4444' : accent }} />
<span className="text-[10px] font-mono uppercase tracking-wider" style={{ color: 'rgba(255,255,255,0.35)' }}>
{label}
</span>
</div>
<span className="text-lg font-bold text-white tabular-nums">{value}</span>
</div>
);
}
export function BillingHealthCard() {
const { data: health, loading, error, refetch } = useBillingHealth();
// Demo badge ONLY when explicitly opted in via the env flag, OR the API
// hands us back demo fixtures (e.g. when the server side runs without a
// live Stripe key and signals it). Live data must never carry the badge.
const envDemo = isDemoMode();
const showDemo = envDemo || (health?.demo === true);
const summary: BillingHealthSummary | null = health ?? null;
return (
<div
className="rounded-2xl border p-5"
style={{
background: 'rgba(255,255,255,0.02)',
borderColor: 'rgba(255,255,255,0.08)',
}}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4" style={{ color: '#d4a054' }} />
<h2 className="text-sm font-semibold text-white">Billing Health</h2>
{showDemo && (
<span
className="text-[9px] font-mono uppercase tracking-wider px-1.5 py-0.5 rounded"
style={{ background: 'rgba(245,158,11,0.1)', color: '#f59e0b' }}
>
demo
</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => refetch()}
className="p-1 rounded transition-colors hover:bg-white/5"
title="Refresh billing health"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} style={{ color: 'rgba(255,255,255,0.3)' }} />
</button>
<Link href="/account/billing" className="text-[11px] transition-opacity hover:opacity-70" style={{ color: '#d4a054' }}>
View billing →
</Link>
</div>
</div>
{summary ? (
<>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<MetricChip
icon={DollarSign}
label="MRR"
value={fmtCurrency(summary.mrr, summary.mrrCurrency)}
accent="#22c55e"
/>
<MetricChip
icon={FileText}
label="Open invoices"
value={fmtCurrency(summary.openInvoicesTotal, summary.openInvoicesCurrency)}
accent="#d4a054"
alert={summary.openInvoicesTotal > 0}
/>
<MetricChip
icon={AlertTriangle}
label="Past due"
value={summary.pastDueCount}
accent="#ef4444"
alert={summary.pastDueCount > 0}
/>
<MetricChip
icon={RotateCcw}
label="Refund queue"
value={summary.refundQueueDepth}
accent="#8b5cf6"
alert={summary.refundQueueDepth > 0}
/>
</div>
{(summary.pastDueCount > 0 || summary.refundQueueDepth > 0) && (
<div
className="mt-3 flex items-center gap-2 text-[12px] px-3 py-2 rounded-lg"
style={{ background: 'rgba(239,68,68,0.06)', color: '#fca5a5' }}
>
<AlertTriangle className="w-3.5 h-3.5 shrink-0 text-red-400" />
{summary.pastDueCount > 0 && (
<span>
{summary.pastDueCount} past-due {summary.pastDueCount === 1 ? 'account requires' : 'accounts require'} attention.
</span>
)}
{summary.refundQueueDepth > 0 && (
<span className="ml-1">
{summary.refundQueueDepth} refund {summary.refundQueueDepth === 1 ? 'request' : 'requests'} pending.
</span>
)}
</div>
)}
</>
) : (
<div
className="flex items-center gap-2 text-[12px] px-3 py-3"
style={{ color: 'rgba(255,255,255,0.35)' }}
>
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
<span>
{error
? `Billing data unavailable — ${error}`
: loading
? 'Loading billing data…'
: 'Billing data unavailable — configure the API server to see live metrics.'}
</span>
</div>
)}
</div>
);
}