259 lines
14 KiB
TypeScript
259 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
|
|
interface Pkg {
|
|
id: string;
|
|
name: string;
|
|
price: string;
|
|
features: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
interface Tenant {
|
|
id: string;
|
|
name: string;
|
|
licenseNumber?: string;
|
|
packageId?: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
import { useDictionary } from "@/lib/dictionary";
|
|
import { useOmni } from "@/components/OmniSyncProvider";
|
|
|
|
export default function BillingManagementPage() {
|
|
const { t } = useDictionary();
|
|
const { currency, setCurrency, formatCurrency, locale, setLocale, theme, setTheme } = useOmni();
|
|
const [packages, setPackages] = useState<Pkg[]>([]);
|
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [stats, setStats] = useState({ totalRevenue: 150000000, activeTenants: 0, growth: "+12.5%" });
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const [pkgResp, eyeResp] = await Promise.all([
|
|
fetch("/api/superadmin/packages"),
|
|
fetch("/api/superadmin/supreme-dashboard")
|
|
]);
|
|
const pkgData = await pkgResp.json();
|
|
const eyeData = await eyeResp.json();
|
|
|
|
setPackages(pkgData.packages || []);
|
|
setTenants(eyeData.matrix || []);
|
|
|
|
// Calculate stats
|
|
const activeCount = (eyeData.matrix || []).filter((t: Tenant) => t.isActive).length;
|
|
setStats(prev => ({ ...prev, activeTenants: activeCount }));
|
|
|
|
} catch (_e) {
|
|
console.error("Failed to load billing data");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const handleKillSwitch = async (id: string, name: string) => {
|
|
if (!confirm(`ACTIVATE KILL-SWITCH: Deactivate [${name}] immediately?`)) return;
|
|
try {
|
|
const res = await fetch('/api/superadmin/tenants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'update', id, isActive: false })
|
|
});
|
|
if (res.ok) {
|
|
setTenants(prev => prev.map(t => t.id === id ? { ...t, isActive: false } : t));
|
|
}
|
|
} catch (_e) {}
|
|
};
|
|
|
|
const handleReactivate = async (id: string) => {
|
|
try {
|
|
const res = await fetch('/api/superadmin/tenants', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'update', id, isActive: true })
|
|
});
|
|
if (res.ok) {
|
|
setTenants(prev => prev.map(t => t.id === id ? { ...t, isActive: true } : t));
|
|
}
|
|
} catch (_e) {}
|
|
};
|
|
|
|
if (loading) return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-white font-mono animate-pulse">SYNCHRONIZING BILLING MATRIX...</div>;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#050b14] p-6 md:p-12 text-white overflow-x-hidden relative">
|
|
{/* BACKGROUND DECORATION */}
|
|
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[150px] -z-10"></div>
|
|
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-purple-500/10 rounded-full blur-[150px] -z-10"></div>
|
|
|
|
<div className="max-w-7xl mx-auto space-y-10">
|
|
|
|
{/* HEADER */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-amber-500 rounded-lg flex items-center justify-center shadow-[0_0_20px_rgba(245,158,11,0.4)]">
|
|
<svg className="w-6 h-6 text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
</div>
|
|
<h1 className="text-4xl font-black tracking-tighter bg-linear-to-r from-amber-400 to-amber-600 bg-clip-text text-transparent">XCU {t('Billing.title')}</h1>
|
|
</div>
|
|
<p className="text-gray-400 font-medium">Quantum Revenue & Subscription Orchestration Node.</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* SETTINGS MATRIX */}
|
|
<div className="flex items-center gap-2 p-1.5 glass-panel rounded-xl border-white/5 bg-black/20">
|
|
<button onClick={() => setLocale(locale === 'id' ? 'en' : 'id')} className="px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-[10px] font-black uppercase tracking-widest border border-white/5 transition-all">
|
|
{locale === 'id' ? 'ID 🇮🇩' : 'EN 🇺🇸'}
|
|
</button>
|
|
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/5 transition-all text-amber-500">
|
|
{theme === 'dark' ? (
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"></path></svg>
|
|
) : (
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
|
)}
|
|
</button>
|
|
<select value={currency} onChange={(e) => setCurrency(e.target.value as any)} className="bg-transparent text-[10px] font-black uppercase tracking-widest border-none focus:ring-0 cursor-pointer text-emerald-500">
|
|
<option value="Rp" className="bg-[#050b14]">IDR</option>
|
|
<option value="USD" className="bg-[#050b14]">USD</option>
|
|
<option value="Crypto" className="bg-[#050b14]">XCU</option>
|
|
</select>
|
|
</div>
|
|
|
|
<Link href="/supreme-admin" className="px-6 py-3 glass-panel border border-white/10 hover:border-white/20 rounded-2xl font-bold transition-all hover:bg-white/5 flex items-center gap-2">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
|
{t('Common.home')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* STATS GRID */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
|
{[
|
|
{ label: "ESTIMATED REVENUE", value: formatCurrency(stats.totalRevenue), color: "text-emerald-400", sub: "Calculated Real-time" },
|
|
{ label: "ACTIVE TENANTS", value: stats.activeTenants.toString(), color: "text-blue-400", sub: stats.growth + " this month" },
|
|
{ label: "SYSTEM UPTIME", value: "99.999%", color: "text-purple-400", sub: "0ms Latency Logic" }
|
|
].map((s, i) => (
|
|
<div key={i} className="glass-panel p-6 rounded-3xl border border-white/5 bg-white/2 overflow-hidden relative group">
|
|
<div className="absolute inset-0 bg-linear-to-br from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
<div className="text-[10px] font-black text-gray-500 tracking-[0.2em] mb-2">{s.label}</div>
|
|
<div className={`text-3xl font-black ${s.color} tracking-tight`}>{s.value}</div>
|
|
<div className="text-[10px] text-gray-500 mt-2">{s.sub}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* ACTIVE PACKAGES */}
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-black tracking-widest text-gray-400 border-l-4 border-amber-500 pl-4">LIVE SUBSCRIPTION PLANS</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
{packages.map(pkg => {
|
|
const numericPrice = parseInt(pkg.price.replace(/[^0-9]/g, '') || '0');
|
|
return (
|
|
<div key={pkg.id} className="glass-panel p-8 rounded-[2rem] border border-white/10 relative overflow-hidden group hover:border-amber-500/50 transition-all">
|
|
<div className="absolute -top-10 -right-10 w-32 h-32 bg-amber-500/5 rounded-full blur-3xl group-hover:bg-amber-500/10 transition-colors"></div>
|
|
<h3 className="text-xl font-extrabold text-white mb-1">{pkg.name}</h3>
|
|
<div className="text-2xl font-black text-amber-500 mb-6">{numericPrice === 0 ? 'PARTNER FREE' : formatCurrency(numericPrice)}</div>
|
|
|
|
<div className="space-y-3 mb-8">
|
|
{JSON.parse(pkg.features || '[]').slice(0, 3).map((f: string, idx: number) => (
|
|
<div key={idx} className="flex items-center gap-2 text-xs text-gray-400">
|
|
<svg className="w-4 h-4 text-emerald-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"></path></svg>
|
|
{f.replace(/\./g, ' ').split(' ').slice(-1)[0].toUpperCase()}
|
|
</div>
|
|
))}
|
|
{JSON.parse(pkg.features || '[]').length > 3 && (
|
|
<div className="text-[10px] text-gray-600 font-bold">+{JSON.parse(pkg.features || '[]').length - 3} MORE CAPABILITIES</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-white/5">
|
|
<div className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Status: <span className="text-emerald-500">PROVISIONED</span></div>
|
|
</div>
|
|
</div>
|
|
)})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* TENANT TABLE */}
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-black tracking-widest text-gray-400 border-l-4 border-blue-500 pl-4">TENANT SUBSCRIPTION REGISTRY</h2>
|
|
<div className="glass-panel rounded-[2rem] border border-white/5 overflow-hidden">
|
|
<table className="w-full text-left">
|
|
<thead>
|
|
<tr className="bg-white/5 text-[10px] font-black text-gray-400 uppercase tracking-[0.2em]">
|
|
<th className="px-8 py-6">Identity</th>
|
|
<th className="px-8 py-6">License Number</th>
|
|
<th className="px-8 py-6">Provision Date</th>
|
|
<th className="px-8 py-6">Status</th>
|
|
<th className="px-8 py-6 text-right">Emergency Control</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/5">
|
|
{tenants.map(t => (
|
|
<tr key={t.id} className="hover:bg-white/[0.02] transition-colors group">
|
|
<td className="px-8 py-6">
|
|
<div className="text-sm font-bold text-white">{t.name}</div>
|
|
<div className="text-[10px] text-gray-500 font-mono mt-1">UID: {t.id.substring(0, 8)}...</div>
|
|
</td>
|
|
<td className="px-8 py-6">
|
|
<span className="text-[10px] font-mono bg-blue-500/10 text-blue-400 px-3 py-1 rounded-full border border-blue-500/20">
|
|
{t.licenseNumber || "N/A"}
|
|
</span>
|
|
</td>
|
|
<td className="px-8 py-6 text-xs text-gray-400 font-mono">
|
|
{new Date(t.createdAt).toLocaleDateString()}
|
|
</td>
|
|
<td className="px-8 py-6">
|
|
{t.isActive ? (
|
|
<div className="flex items-center gap-2 text-[10px] font-black text-emerald-400 uppercase tracking-widest">
|
|
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.5)]"></div>
|
|
ACTIVE
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-[10px] font-black text-red-500 uppercase tracking-widest">
|
|
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
|
|
SUSPENDED
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-8 py-6 text-right">
|
|
{t.isActive ? (
|
|
<button
|
|
onClick={() => handleKillSwitch(t.id, t.name)}
|
|
className="text-[10px] font-black text-red-500 border border-red-500/30 px-4 py-2 rounded-xl hover:bg-red-500 hover:text-white transition-all shadow-[0_0_10px_rgba(239,68,68,0)] hover:shadow-[0_0_15px_rgba(239,68,68,0.5)] uppercase tracking-widest"
|
|
>
|
|
Kill Switch
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => handleReactivate(t.id)}
|
|
className="text-[10px] font-black text-emerald-500 border border-emerald-500/30 px-4 py-2 rounded-xl hover:bg-emerald-500 hover:text-white transition-all uppercase tracking-widest"
|
|
>
|
|
Restore Access
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* FOOTER WATERMARK */}
|
|
<div className="mt-20 text-center pb-10">
|
|
<div className="text-[10px] font-black text-gray-700 tracking-[0.5em] uppercase">JUMPA.ID ULTRA BILLING SYSTEM V2.4</div>
|
|
<div className="text-[8px] text-gray-800 mt-2 font-mono">ENCRYPTED QUANTUM HANDSHAKE: SUCCESS</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|