feat(CC-082): add Billing & Plan page (infra cost breakdown + transaction lookup)
This commit is contained in:
parent
2dc44abcfa
commit
cdce06b909
1 changed files with 125 additions and 0 deletions
125
src/app/dashboard/settings/billing/page.tsx
Normal file
125
src/app/dashboard/settings/billing/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue