feat(CC-082): add Billing & Plan page (infra cost breakdown + transaction lookup)

This commit is contained in:
admin-valentin 2026-08-02 17:16:29 +00:00
parent 2dc44abcfa
commit cdce06b909

View file

@ -0,0 +1,125 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
import { apiFetch } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
interface Transaction { id: string; amount: string; type: string; description: string | null; category: string | null; date: string; currency: string | null; }
const PLAN_FEATURES = [
{ label: 'CEO OS Platform', included: true },
{ label: 'Hetzner CAX41 (16vCPU/32GB)', included: true },
{ label: 'Coolify self-hosted', included: true },
{ label: 'AnythingLLM + ClickHouse', included: true },
{ label: 'n8n self-hosted', included: true },
{ label: 'Forgejo + Hermes', included: true },
{ label: 'Claude Pro', included: true },
{ label: 'ChatGPT Plus + Gemini', included: true },
{ label: 'OpenRouter credit', included: true },
];
const INFRA_COSTS = [
{ name: 'Hetzner CAX41', cost: 35, currency: 'EUR', period: 'lună' },
{ name: 'Claude Pro', cost: 20, currency: 'USD', period: 'lună' },
{ name: 'ChatGPT Plus', cost: 20, currency: 'USD', period: 'lună' },
{ name: 'Gemini Advanced', cost: 20, currency: 'USD', period: 'lună' },
{ name: 'OpenRouter credit', cost: 15, currency: 'USD', period: 'lună (estimat)' },
];
export default function BillingPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: transactions = [] } = useQuery({
queryKey: ['billing-txns', tenantId],
queryFn: () => apiFetch<Transaction[]>('/v1/transactions?limit=100', { tenantId }),
enabled: Boolean(tenantId), staleTime: 120_000,
});
const infraCosts = transactions.filter((t) => {
const desc = (t.description ?? t.category ?? '').toLowerCase();
return desc.includes('hetzner') || desc.includes('claude') || desc.includes('openai') || desc.includes('openrouter') || desc.includes('gemini') || desc.includes('anthropic');
});
const totalMonthlyEUR = INFRA_COSTS.filter((c) => c.currency === 'EUR').reduce((s, c) => s + c.cost, 0);
const totalMonthlyUSD = INFRA_COSTS.filter((c) => c.currency === 'USD').reduce((s, c) => s + c.cost, 0);
return (
<div className="max-w-3xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Facturare & Plan</h1>
<p className="text-sm text-ink-faint mt-1">Overview costuri infrastructură și plan CEO OS.</p>
</div>
{/* Current plan */}
<div className="card p-5 border-primary/30 bg-primary/5 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="text-base font-bold text-ink">CEO OS Self-Hosted</p>
<p className="text-xs text-ink-faint">Plan personal AI Impact SRL + ADA MarkAI UG</p>
</div>
<span className="rounded-full bg-signal-ok/10 text-signal-ok px-3 py-1 text-xs font-bold">Activ</span>
</div>
<div className="grid grid-cols-2 gap-1">
{PLAN_FEATURES.map((f) => (
<div key={f.label} className="flex items-center gap-1.5 text-[10px]">
<span className={f.included ? 'text-signal-ok' : 'text-ink-faint'}>{f.included ? '✓' : '○'}</span>
<span className={f.included ? 'text-ink' : 'text-ink-faint'}>{f.label}</span>
</div>
))}
</div>
</div>
{/* Cost breakdown */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Costuri infrastructură estimate</p>
<div className="space-y-2">
{INFRA_COSTS.map((c) => (
<div key={c.name} className="flex items-center justify-between text-sm">
<span className="text-ink-faint">{c.name}</span>
<span className="font-medium text-ink">{c.cost} {c.currency}/{c.period}</span>
</div>
))}
<div className="pt-2 border-t border-border/50 space-y-1">
<div className="flex items-center justify-between text-sm font-bold">
<span className="text-ink">Total EUR</span>
<span className="text-ink"> {totalMonthlyEUR} EUR/lună</span>
</div>
<div className="flex items-center justify-between text-sm font-bold">
<span className="text-ink">Total USD</span>
<span className="text-ink"> {totalMonthlyUSD} USD/lună</span>
</div>
</div>
</div>
</div>
{/* From transactions */}
{infraCosts.length > 0 && (
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Plăți infrastructură din tranzacții ({infraCosts.length})
</p>
<div className="divide-y divide-border/50">
{infraCosts.slice(0, 10).map((t) => (
<div key={t.id} className="flex items-center justify-between py-2">
<div>
<p className="text-xs text-ink">{t.description ?? t.category}</p>
<p className="text-[10px] text-ink-faint">{new Date(t.date).toLocaleDateString('ro-RO')}</p>
</div>
<span className="text-xs font-medium text-signal-danger">
{parseFloat(t.amount) < 0 ? '' : '-'}{Math.abs(parseFloat(t.amount)).toFixed(2)} {t.currency ?? 'RON'}
</span>
</div>
))}
</div>
</div>
)}
<div className="flex gap-3">
<Link href="/dashboard/finances" className="text-xs text-primary hover:underline">Toate tranzacțiile </Link>
<Link href="/dashboard/settings" className="text-xs text-ink-faint hover:underline">Setări</Link>
</div>
</div>
);
}