876 lines
53 KiB
TypeScript
876 lines
53 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface Feature {
|
|
key: string;
|
|
name: string;
|
|
module?: string;
|
|
}
|
|
|
|
interface Package {
|
|
id: string;
|
|
name: string;
|
|
price: string;
|
|
features: string;
|
|
}
|
|
|
|
interface Tenant {
|
|
id: string;
|
|
name: string;
|
|
licenseNumber: string;
|
|
licenses?: string | Record<string, string>;
|
|
package?: {
|
|
id: string;
|
|
name: string;
|
|
features: string;
|
|
};
|
|
byokEnabled?: boolean;
|
|
byokKey?: string;
|
|
users?: any[];
|
|
}
|
|
|
|
interface User {
|
|
email: string;
|
|
role: string;
|
|
}
|
|
|
|
export default function SupremeAdminDashboard() {
|
|
const [data, setData] = useState<User | null>(null);
|
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
|
const [packages, setPackages] = useState<Package[]>([]);
|
|
const [systemFeatures, setSystemFeatures] = useState<Feature[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const router = useRouter();
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
window.location.href = '/';
|
|
} catch (e) {
|
|
console.error("Logout failed", e);
|
|
window.location.href = '/';
|
|
}
|
|
};
|
|
|
|
// Modal States
|
|
const [showPackageModal, setShowPackageModal] = useState(false);
|
|
const [showTenantModal, setShowTenantModal] = useState(false);
|
|
const [selectedTenant, setSelectedTenant] = useState<Tenant | null>(null);
|
|
|
|
// Quantum Toast System
|
|
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null);
|
|
|
|
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
|
setToast({ message, type });
|
|
setTimeout(() => setToast(null), 4000);
|
|
};
|
|
const [tenantLicenses, setTenantLicenses] = useState<Record<string, string>>({});
|
|
const [byokEnabled, setByokEnabled] = useState(false);
|
|
const [byokKey, setByokKey] = useState("");
|
|
|
|
// Form States
|
|
const [editingPkgId, setEditingPkgId] = useState<string | null>(null);
|
|
const [newPkgName, setNewPkgName] = useState("");
|
|
const [newPkgPrice, setNewPkgPrice] = useState("");
|
|
const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
// XCU Engine State
|
|
const [xcuNodes, setXcuNodes] = useState<any[]>([]);
|
|
const [xcuLoading, setXcuLoading] = useState(false);
|
|
|
|
// Security Tier Modal
|
|
const [tierModal, setTierModal] = useState<{tenantId: string; tenantName: string; currentTier: string} | null>(null);
|
|
const [tierSwitching, setTierSwitching] = useState(false);
|
|
const [selectedTier, setSelectedTier] = useState('STANDARD');
|
|
|
|
const TIER_CONFIG: Record<string, {label:string; desc:string; color:string; bg:string; border:string; icon:string}> = {
|
|
STANDARD: { label: "Let's Encrypt", desc: 'Sertifikat global, dipercaya semua browser. Auto-renew 90 hari.', color: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', icon: '🌐' },
|
|
SOVEREIGN: { label: 'Private CA (X)', desc: 'RSA-4096, 30 tahun. Offline-ready, tidak bergantung internet global.', color: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', icon: '🛡️' },
|
|
CLIENT_CA: { label: 'Client CA (Upload)', desc: 'Tenant upload CA mereka sendiri (BSSN/Komdigi/Militer/BIN).', color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20', icon: '🏛️' },
|
|
};
|
|
|
|
const fetchXcuStatus = useCallback(async () => {
|
|
setXcuLoading(true);
|
|
try {
|
|
const resp = await fetch('/api/superadmin/xcu-tls-switch');
|
|
const data = await resp.json();
|
|
setXcuNodes(data.nodes || []);
|
|
} catch (_e) {
|
|
console.error('XCU fetch failed', _e);
|
|
} finally {
|
|
setXcuLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const fetchData = useCallback(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();
|
|
|
|
if (eyeData.error) {
|
|
router.push("/");
|
|
return;
|
|
}
|
|
|
|
setPackages(pkgData.packages || []);
|
|
setTenants(eyeData.matrix || []);
|
|
setSystemFeatures(pkgData.systemFeatures || []);
|
|
setData(eyeData.user || { email: "Supreme Admin", role: "superadmin" });
|
|
} catch (_e) {
|
|
console.error(_e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [router]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
|
|
// Live polling XCU Engine status every 5s
|
|
const pollXcu = async () => {
|
|
try {
|
|
const resp = await fetch('/api/superadmin/xcu-tls-switch');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
if (data.nodes) setXcuNodes(data.nodes);
|
|
}
|
|
} catch {}
|
|
setXcuLoading(false);
|
|
};
|
|
pollXcu();
|
|
const xcuInterval = setInterval(pollXcu, 5000);
|
|
return () => clearInterval(xcuInterval);
|
|
}, [fetchData]);
|
|
|
|
const handleSavePackage = async (e?: React.FormEvent | React.MouseEvent) => {
|
|
if (e) e.preventDefault();
|
|
try {
|
|
const resp = await fetch("/api/superadmin/packages", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: editingPkgId ? "update" : "create",
|
|
id: editingPkgId,
|
|
name: newPkgName,
|
|
price: newPkgPrice,
|
|
features: selectedFeatures
|
|
})
|
|
});
|
|
if (resp.ok) {
|
|
setShowPackageModal(false);
|
|
setEditingPkgId(null);
|
|
fetchData();
|
|
showToast(editingPkgId ? "Paket berhasil diperbarui." : "Paket berhasil dibuat.");
|
|
} else {
|
|
const errorData = await resp.json();
|
|
showToast(errorData.error || "Gagal menyimpan paket.", "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Error menyimpan paket. Periksa koneksi Anda.", "error");
|
|
}
|
|
};
|
|
|
|
const handleDeletePackage = async (id: string) => {
|
|
try {
|
|
const resp = await fetch("/api/superadmin/packages", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "delete", id })
|
|
});
|
|
if (resp.ok) {
|
|
fetchData();
|
|
showToast("Paket berhasil dihapus.");
|
|
} else {
|
|
const errorData = await resp.json();
|
|
showToast(errorData.error || "Gagal menghapus paket.", "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Error menghapus paket. Periksa koneksi Anda.", "error");
|
|
}
|
|
};
|
|
|
|
const saveTenantPackage = async (tenantId: string, packageId: string) => {
|
|
try {
|
|
const resp = await fetch("/api/superadmin/supreme-dashboard", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: "update_tenant_package",
|
|
tenantId,
|
|
packageId
|
|
})
|
|
});
|
|
if (resp.ok) {
|
|
fetchData();
|
|
showToast("Paket tenant berhasil diperbarui.");
|
|
} else {
|
|
const errorData = await resp.json();
|
|
showToast(errorData.error || "Gagal menetapkan paket.", "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Gagal menetapkan paket. Periksa koneksi.", "error");
|
|
}
|
|
};
|
|
|
|
const openTenantMatrix = (tenant: Tenant) => {
|
|
setSelectedTenant(tenant);
|
|
setByokEnabled(tenant.byokEnabled || false);
|
|
setByokKey(tenant.byokKey || "");
|
|
let parsed: Record<string, string> = {};
|
|
try {
|
|
if (typeof tenant.licenses === 'string') parsed = JSON.parse(tenant.licenses);
|
|
else parsed = tenant.licenses || {};
|
|
|
|
if (Array.isArray(parsed)) {
|
|
const temp: Record<string, string> = {};
|
|
parsed.forEach((k: string) => temp[k] = "GRANTED");
|
|
parsed = temp;
|
|
}
|
|
} catch(_e) {}
|
|
setTenantLicenses(parsed);
|
|
setShowTenantModal(true);
|
|
};
|
|
|
|
const [isSavingMatrix, setIsSavingMatrix] = useState(false);
|
|
|
|
const saveTenantMatrix = async () => {
|
|
setIsSavingMatrix(true);
|
|
try {
|
|
const resp = await fetch("/api/superadmin/supreme-dashboard", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: "update_tenant_licenses",
|
|
tenantId: selectedTenant?.id,
|
|
licenses: tenantLicenses,
|
|
byokEnabled,
|
|
byokKey
|
|
})
|
|
});
|
|
if (resp.ok) {
|
|
setShowTenantModal(false);
|
|
fetchData();
|
|
showToast("Matriks Sovereignty berhasil di-deploy tanpa henti (Zero Downtime).");
|
|
} else {
|
|
const errorData = await resp.json();
|
|
showToast(errorData.error || "Gagal menyimpan matrix.", "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Koneksi terputus. Gagal menyimpan matrix.", "error");
|
|
} finally {
|
|
setIsSavingMatrix(false);
|
|
}
|
|
};
|
|
|
|
const applySecurityTier = async () => {
|
|
if (!tierModal || selectedTier === tierModal.currentTier) return;
|
|
setTierSwitching(true);
|
|
try {
|
|
const resp = await fetch('/api/superadmin/supreme-dashboard', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'update_security_tier', tenantId: tierModal.tenantId, securityTier: selectedTier }),
|
|
});
|
|
if (resp.ok) {
|
|
fetchData();
|
|
showToast(`Security Tier berhasil di-switch ke ${TIER_CONFIG[selectedTier]?.label || selectedTier}`);
|
|
setTierModal(null);
|
|
} else {
|
|
showToast('Gagal mengubah Security Tier', 'error');
|
|
}
|
|
} catch { showToast('Koneksi gagal', 'error'); }
|
|
setTierSwitching(false);
|
|
};
|
|
|
|
if (loading) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-blue-600 font-medium">Loading Supreme Admin...</div>;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#f4f5f9] text-gray-900 font-sans pb-20 relative">
|
|
|
|
{/* Quantum Toast Notification System */}
|
|
{toast && (
|
|
<div className={`fixed top-20 right-8 z-[100] px-6 py-4 rounded-xl shadow-2xl backdrop-blur-xl border flex items-center gap-4 animate-in slide-in-from-right-8 fade-in duration-300 ${toast.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-700' : 'bg-red-500/10 border-red-500/30 text-red-700'}`}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${toast.type === 'success' ? 'bg-emerald-500 text-white' : 'bg-red-500 text-white'}`}>
|
|
{toast.type === 'success' ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
|
|
) : (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="text-xs font-black uppercase tracking-widest opacity-80 mb-0.5">{toast.type === 'success' ? 'System Success' : 'System Alert'}</div>
|
|
<div className="text-sm font-medium">{toast.message}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<header className="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-8 sticky top-0 z-40 shadow-sm">
|
|
<div className="flex items-center gap-3">
|
|
<svg className="w-7 h-7 text-blue-600" fill="currentColor" viewBox="0 0 24 24"><path d="M4 4h10a2 2 0 0 1 2 2v3.5l4-3v11l-4-3V18a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"></path></svg>
|
|
<h1 className="text-xl font-bold tracking-tight text-gray-800">Supreme Admin <span className="text-sm font-normal text-gray-500 ml-2">Workspace Control</span></h1>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<a href="/supreme-admin/telepathy" className="px-3 py-1.5 bg-gradient-to-r from-[#ff0080] to-[#7928ca] text-white text-xs font-bold rounded-md hover:opacity-90 transition-opacity flex items-center gap-2 shadow-sm">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
TELEPATHY MATRIX
|
|
</a>
|
|
<a href="/supreme-admin/telemetry" className="px-3 py-1.5 bg-gradient-to-r from-[#25D366] to-[#0b5cff] text-white text-xs font-bold rounded-md hover:opacity-90 transition-opacity flex items-center gap-2 shadow-sm">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
PANOPTICON
|
|
</a>
|
|
<span className="text-sm text-gray-600 font-medium">{data?.email}</span>
|
|
<button onClick={handleLogout} className="text-sm text-blue-600 hover:text-blue-800 font-semibold transition-colors">Sign Out</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-6 py-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
|
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Total Tenants</div>
|
|
<div className="text-3xl font-bold text-gray-900">{tenants.length}</div>
|
|
</div>
|
|
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
|
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">XCU Modules</div>
|
|
<div className="text-3xl font-bold text-gray-900">{systemFeatures.length || '101'}</div>
|
|
</div>
|
|
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
|
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Active Packages</div>
|
|
<div className="text-3xl font-bold text-gray-900">{packages.length}</div>
|
|
</div>
|
|
<div className="bg-blue-600 p-5 rounded-lg border border-blue-700 shadow-sm text-white flex flex-col justify-center cursor-pointer hover:bg-blue-700 transition-colors" onClick={() => { setEditingPkgId(null); setNewPkgName(""); setNewPkgPrice(""); setSelectedFeatures([]); setShowPackageModal(true); }}>
|
|
<div className="text-sm font-bold flex items-center justify-between">
|
|
Create New Package
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4"></path></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* XCU QUIC ENGINE STATUS PANEL */}
|
|
<div className="mb-8 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 rounded-2xl border border-slate-700/50 shadow-xl overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-slate-700/50 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center shadow-lg shadow-cyan-500/20">
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-base font-bold text-white tracking-tight">XCom ULTRA Engine</h2>
|
|
<p className="text-[10px] text-slate-400 font-mono">QUIC/WebTransport • Port 8443 (UDP) • Port 8081 (HTTP)</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-red-500/10 border border-red-500/20 rounded-lg">
|
|
<span className="relative flex h-2.5 w-2.5">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"></span>
|
|
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-red-500"></span>
|
|
</span>
|
|
<span className="text-[10px] font-bold uppercase tracking-wider text-red-400">LIVE</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-5">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{xcuNodes.length > 0 ? xcuNodes.map((node: any, idx: number) => (
|
|
<div key={idx} className={`relative rounded-xl border p-4 transition-all duration-300 ${
|
|
node.online
|
|
? 'bg-slate-800/80 border-slate-600/50 hover:border-cyan-500/50 hover:shadow-lg hover:shadow-cyan-500/5'
|
|
: 'bg-red-950/30 border-red-800/30'
|
|
}`}>
|
|
{/* Pulse indicator */}
|
|
<div className="absolute top-3 right-3">
|
|
<span className={`relative flex h-3 w-3`}>
|
|
{node.online && <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>}
|
|
<span className={`relative inline-flex rounded-full h-3 w-3 ${node.online ? 'bg-emerald-400' : 'bg-red-500'}`}></span>
|
|
</span>
|
|
</div>
|
|
{/* Node name */}
|
|
<div className="text-xs font-black text-slate-400 uppercase tracking-widest mb-2">{node.name}</div>
|
|
<div className="text-[10px] font-mono text-slate-500 mb-3">{node.host}</div>
|
|
{node.online ? (
|
|
<>
|
|
{/* TLS Mode Badge */}
|
|
<div className="mb-3">
|
|
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold uppercase tracking-wider ${
|
|
node.tlsMode === 'SELFSIGNED'
|
|
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
|
: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
|
}`}>
|
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
TLS: {node.tlsMode || 'LETSENCRYPT'}
|
|
</div>
|
|
</div>
|
|
{/* Metrics */}
|
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
|
<div className="bg-slate-900/50 rounded-lg p-2">
|
|
<div className="text-[9px] text-slate-500 uppercase font-bold">CPU</div>
|
|
<div className="text-sm font-bold text-cyan-400">{typeof node.cpu === 'number' ? node.cpu.toFixed(1) : '0'}%</div>
|
|
</div>
|
|
<div className="bg-slate-900/50 rounded-lg p-2">
|
|
<div className="text-[9px] text-slate-500 uppercase font-bold">RAM</div>
|
|
<div className="text-sm font-bold text-purple-400">{typeof node.ram === 'number' ? node.ram.toFixed(1) : '0'}%</div>
|
|
</div>
|
|
</div>
|
|
{/* Cert hash */}
|
|
<div className="text-[8px] font-mono text-slate-600 truncate" title={node.certHash}>CERT: {node.certHash?.substring(0, 16)}...</div>
|
|
{/* Status */}
|
|
<div className={`mt-2 text-[10px] font-bold uppercase tracking-wider ${
|
|
node.status === 'SECURE' ? 'text-emerald-400' : node.status === 'OUROBOROS_IGNITED' ? 'text-red-400' : 'text-slate-400'
|
|
}`}>
|
|
● {node.status}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="text-sm font-bold text-red-400 mt-2">● OFFLINE</div>
|
|
)}
|
|
</div>
|
|
)) : (
|
|
<div className="col-span-3 text-center py-8">
|
|
<div className="text-slate-500 text-sm">Loading XCU Engine status...</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-5 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
|
<h2 className="text-lg font-semibold text-gray-800">Client Organizations (Tenants)</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-100">
|
|
{tenants.map((t: Tenant) => {
|
|
const isVIP = t.name.toLowerCase().includes('tsm') || t.name.toLowerCase().includes('snowy');
|
|
return (
|
|
<div key={t.id} className="flex flex-col border-b border-gray-100 last:border-0">
|
|
<div className="p-6 hover:bg-gray-50 transition-colors flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h3 className="text-base font-bold text-gray-900">{t.name}</h3>
|
|
{isVIP && <span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-[10px] font-bold rounded uppercase tracking-wider">VIP Partner</span>}
|
|
{/* Security Tier Badge */}
|
|
{(() => {
|
|
const tier = (t as any).securityTier || 'STANDARD';
|
|
const badgeMap: Record<string,string> = {
|
|
STANDARD: 'bg-emerald-50 text-emerald-600 border-emerald-200',
|
|
SOVEREIGN: 'bg-cyan-50 text-cyan-700 border-cyan-200',
|
|
CLIENT_CA: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
};
|
|
const labelMap: Record<string,string> = { STANDARD: '🌐 LET\'S ENCRYPT', SOVEREIGN: '🛡️ PRIVATE CA', CLIENT_CA: '🏛️ CLIENT CA' };
|
|
return (
|
|
<>
|
|
<button
|
|
onClick={() => { setSelectedTier(tier); setTierModal({ tenantId: t.id, tenantName: t.name, currentTier: tier }); }}
|
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider transition-all hover:scale-105 border ${badgeMap[tier] || badgeMap.STANDARD}`}
|
|
title="Klik untuk ubah Security Tier"
|
|
>
|
|
{labelMap[tier] || '🌐 STANDARD'}
|
|
</button>
|
|
{(tier === 'SOVEREIGN' || tier === 'CLIENT_CA') && (
|
|
<a href="/supreme-admin/sovereign-setup" className={`px-2 py-0.5 text-white text-[10px] font-bold rounded transition-colors ${tier === 'CLIENT_CA' ? 'bg-amber-500 hover:bg-amber-600' : 'bg-cyan-600 hover:bg-cyan-700'}`}>CA Setup →</a>
|
|
)}
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
<div className="text-xs text-gray-500 font-mono mb-2 flex items-center gap-2">
|
|
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
|
License: <strong className="text-gray-700">{t.licenseNumber || 'PENDING'}</strong>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col sm:items-end gap-3 w-full sm:w-auto">
|
|
<div className="flex flex-col items-end gap-1 w-full">
|
|
<span className="text-[10px] font-bold text-gray-500 uppercase">Assigned Package</span>
|
|
<select
|
|
className="text-xs border rounded px-2 py-1 bg-gray-50 w-full sm:w-48 outline-none focus:ring-1 focus:ring-blue-500"
|
|
value={t.package?.id || ""}
|
|
onChange={e => saveTenantPackage(t.id, e.target.value)}
|
|
>
|
|
<option value="">-- No Package (Custom) --</option>
|
|
{packages.map(p => (
|
|
<option key={p.id} value={p.id}>{p.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<button onClick={() => openTenantMatrix(t)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors self-start sm:self-auto">
|
|
Manage Feature Matrix & BYOK
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/* Render Users List for the Tenant */}
|
|
{t.users && t.users.length > 0 && (
|
|
<div className="bg-gray-50 border-t border-gray-100 p-4 pl-10">
|
|
<div className="text-[11px] font-bold text-gray-500 uppercase tracking-widest mb-2 flex items-center gap-2">
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
Registered Neural Entities
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
{t.users.map((u: any) => (
|
|
<div key={u.id} className="flex justify-between items-center bg-white border border-gray-200 p-2 rounded-md shadow-sm">
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-semibold text-gray-800">{u.email}</span>
|
|
<span className="text-[10px] text-gray-400 font-mono">{u.id}</span>
|
|
</div>
|
|
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${u.role === 'admin' ? 'bg-purple-100 text-purple-700' : u.role === 'superadmin' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}`}>
|
|
{u.role}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-1 space-y-6">
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
<div className="px-6 py-5 border-b border-gray-200 bg-gray-50">
|
|
<h2 className="text-lg font-semibold text-gray-800">Module Packages</h2>
|
|
</div>
|
|
<div className="p-4 space-y-3">
|
|
{packages.map((p: Package) => {
|
|
let featList: string[] = [];
|
|
try {
|
|
featList = JSON.parse(p.features || "[]");
|
|
} catch (e) {}
|
|
|
|
const isJVC = p.name.includes("JVC");
|
|
const isJC = p.name.includes("JC");
|
|
const isCombo = isJVC && isJC;
|
|
|
|
const bgClass = isCombo ? "bg-gradient-to-br from-blue-800 via-teal-700 to-emerald-600 text-white border-none shadow-2xl shadow-teal-900/40 ring-1 ring-teal-400/50" : isJVC ? "bg-gradient-to-br from-indigo-950 via-blue-900 to-indigo-900 text-white border-none shadow-xl shadow-blue-900/20" : isJC ? "bg-gradient-to-br from-teal-950 via-emerald-900 to-teal-900 text-white border-none shadow-xl shadow-emerald-900/20" : "bg-white border border-gray-200 text-gray-900 shadow-sm";
|
|
const badgeClass = isCombo ? "bg-white/20 text-white border border-white/30 font-bold drop-shadow-sm" : isJVC ? "bg-blue-500/20 text-blue-100 border border-blue-400/30" : isJC ? "bg-emerald-500/20 text-emerald-100 border border-emerald-400/30" : "bg-gray-50 text-gray-700 border border-gray-200";
|
|
const textMuted = isJVC || isJC || isCombo ? "text-gray-300" : "text-gray-500";
|
|
|
|
return (
|
|
<div key={p.id} className={`p-5 rounded-2xl transition-all duration-300 hover:-translate-y-1 relative overflow-hidden group ${bgClass}`}>
|
|
{/* Decorative background glow */}
|
|
{(isJVC || isJC || isCombo) && <div className="absolute -top-12 -right-12 w-40 h-40 bg-white opacity-[0.03] blur-3xl rounded-full pointer-events-none group-hover:opacity-10 transition-opacity"></div>}
|
|
{isCombo && <div className="absolute -bottom-10 -left-10 w-32 h-32 bg-emerald-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
|
{isCombo && <div className="absolute -top-10 -right-10 w-32 h-32 bg-blue-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
|
|
|
<div className="flex justify-between items-start mb-4 relative z-10">
|
|
<div className="font-bold text-lg tracking-tight flex items-center gap-2 drop-shadow-md">
|
|
{isCombo && <svg className="w-5 h-5 text-teal-100" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>}
|
|
{p.name}
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1.5">
|
|
<div className={`text-sm font-black ${isJVC || isJC || isCombo ? 'text-white' : 'text-blue-600'}`}>{p.price}</div>
|
|
<div className="flex gap-3 mt-1">
|
|
<button onClick={() => { setEditingPkgId(p.id); setNewPkgName(p.name); setNewPkgPrice(p.price); setSelectedFeatures(featList); setShowPackageModal(true); }} className={`text-[10px] font-bold uppercase tracking-wider hover:underline ${textMuted}`}>Edit</button>
|
|
<button onClick={() => handleDeletePackage(p.id)} className={`text-[10px] font-bold uppercase tracking-wider hover:underline ${isJVC || isJC || isCombo ? 'text-red-300' : 'text-red-500'}`}>Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className={`text-[10px] font-bold uppercase tracking-widest mb-3 border-t pt-4 ${isJVC || isJC || isCombo ? 'border-white/10 text-white/50' : 'border-gray-100 text-gray-400'}`}>
|
|
Core Capabilities ({featList.length} Modules)
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5 relative z-10">
|
|
{featList.length > 0 ? (
|
|
featList.map((f: string) => (
|
|
<span key={f} className={`text-[9.5px] font-mono px-2 py-1 rounded-md backdrop-blur-md ${badgeClass}`}>
|
|
{f}
|
|
</span>
|
|
))
|
|
) : (
|
|
<span className={`text-[10px] italic ${textMuted}`}>No modules defined</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{/* CREATE PACKAGE MODAL */}
|
|
{showPackageModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-white w-full max-w-2xl rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
|
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center bg-gray-50">
|
|
<h2 className="text-xl font-bold text-gray-900">Create XCU Module Package</h2>
|
|
<button onClick={() => setShowPackageModal(false)} className="text-gray-400 hover:text-gray-700">✕</button>
|
|
</div>
|
|
<div className="p-6 overflow-y-auto flex-1">
|
|
<div id="pkgForm" className="space-y-6">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<input type="text" value={newPkgName} onChange={e => setNewPkgName(e.target.value)} placeholder="Package Name" className="border p-2 rounded" required />
|
|
<input type="text" value={newPkgPrice} onChange={e => setNewPkgPrice(e.target.value)} placeholder="Price (Rp)" className="border p-2 rounded" required />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4 border-b pb-2">
|
|
<h3 className="text-sm font-bold">Select Modules</h3>
|
|
<input type="text" placeholder="Search..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="text-xs border rounded-full px-3 py-1 outline-none" />
|
|
</div>
|
|
<div className="space-y-4">
|
|
{['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
|
const filtered = systemFeatures.filter(f => (f.module === mod || (!f.module && mod === 'IAM')) && (f.name.toLowerCase().includes(searchQuery.toLowerCase()) || f.key.toLowerCase().includes(searchQuery.toLowerCase())));
|
|
if (filtered.length === 0) return null;
|
|
return (
|
|
<div key={mod}>
|
|
<div className="text-[10px] font-black text-blue-600 mb-2 flex justify-between">
|
|
<span>{mod} MODULES</span>
|
|
<div className="space-x-2">
|
|
<button type="button" onClick={() => setSelectedFeatures(Array.from(new Set([...selectedFeatures, ...systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).map(f => f.key)])))} className="hover:underline">Select All</button>
|
|
<button type="button" onClick={() => setSelectedFeatures(selectedFeatures.filter(k => !systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).some(f => f.key === k)))} className="hover:underline">Clear</button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{filtered.map(f => (
|
|
<label key={f.key} className="flex items-center gap-2 p-2 border rounded hover:bg-gray-50 cursor-pointer">
|
|
<input type="checkbox" checked={selectedFeatures.includes(f.key)} onChange={e => e.target.checked ? setSelectedFeatures([...selectedFeatures, f.key]) : setSelectedFeatures(selectedFeatures.filter(x => x !== f.key))} />
|
|
<div className="text-[10px]">{f.name}</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 border-t bg-gray-50 flex justify-end gap-2">
|
|
<button type="button" onClick={() => setShowPackageModal(false)} className="px-4 py-2 border rounded">Cancel</button>
|
|
<button type="button" onClick={handleSavePackage} className="px-4 py-2 bg-blue-600 text-white rounded">Save Package</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TENANT MATRIX MODAL */}
|
|
{showTenantModal && selectedTenant && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-white w-full max-w-3xl rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
|
<div className="px-6 py-4 border-b bg-gray-50 flex justify-between items-center relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-full bg-gradient-to-l from-blue-100/50 to-transparent pointer-events-none"></div>
|
|
<div>
|
|
<h2 className="text-2xl font-black text-gray-900 tracking-tight flex items-center gap-3">
|
|
{selectedTenant.name}
|
|
<span className="px-3 py-1 bg-gray-900 text-white text-[10px] font-bold rounded-full uppercase tracking-widest shadow-sm">
|
|
{selectedTenant.package ? selectedTenant.package.name : "A LA CARTE (CUSTOM)"}
|
|
</span>
|
|
</h2>
|
|
<p className="text-xs text-gray-500 font-medium mt-1">Superadmin Matrix Control & Sovereignty Panel</p>
|
|
</div>
|
|
<button onClick={() => setShowTenantModal(false)} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-200 text-gray-500 transition-colors z-10">✕</button>
|
|
</div>
|
|
<div className="p-6 overflow-y-auto flex-1 bg-gradient-to-b from-gray-50/50 to-white">
|
|
|
|
{/* SPECTACULAR LICENSE TIER BANNER */}
|
|
<div className={`mb-6 relative overflow-hidden rounded-2xl p-6 border ${selectedTenant.package ? 'bg-gradient-to-br from-indigo-950 via-blue-900 to-indigo-900 border-none shadow-xl shadow-blue-900/20 text-white' : 'bg-white border-dashed border-gray-300 text-gray-800'}`}>
|
|
{selectedTenant.package && <div className="absolute -top-24 -right-24 w-64 h-64 bg-blue-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
|
{selectedTenant.package && <div className="absolute -bottom-24 -left-24 w-64 h-64 bg-cyan-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
|
|
|
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
<div>
|
|
<div className={`text-[10px] font-black uppercase tracking-widest mb-1 ${selectedTenant.package ? 'text-blue-300' : 'text-gray-400'}`}>Active Quantum Tier</div>
|
|
<div className="text-2xl font-black flex items-center gap-2">
|
|
{selectedTenant.package ? (
|
|
<>
|
|
<svg className="w-6 h-6 text-cyan-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l10-7.27z"/></svg>
|
|
{selectedTenant.package.name}
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
|
Custom A La Carte
|
|
</>
|
|
)}
|
|
</div>
|
|
<p className={`text-xs mt-2 max-w-xl ${selectedTenant.package ? 'text-blue-100' : 'text-gray-500'}`}>
|
|
{selectedTenant.package ?
|
|
"Modul-modul di bawah ini otomatis di-kunci (GRANTED) berdasarkan paket yang dipilih. Anda dapat menambahkan modul tambahan sebagai A La Carte." :
|
|
"Tenant ini tidak terikat pada paket apapun. Semua modul diatur secara manual per-item melalui saklar di bawah ini."}
|
|
</p>
|
|
</div>
|
|
{selectedTenant.package && (
|
|
<div className="px-4 py-2 bg-white/10 backdrop-blur border border-white/20 rounded-xl text-xs font-bold text-blue-50">
|
|
Paket Terkunci (Auto-Granted)
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-blue-50/80 p-6 rounded-2xl border border-blue-100 shadow-inner">
|
|
<div className="flex-1">
|
|
<h3 className="text-sm font-black text-blue-900 uppercase tracking-widest mb-1 flex items-center gap-2">
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd"></path></svg>
|
|
Quantum BYOK Sovereignty
|
|
</h3>
|
|
<p className="text-[10px] text-blue-700 font-medium">Berikan otoritas penuh kepada tenant untuk mendikte kunci enkripsi E2EE mereka sendiri.</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2 w-full md:w-64">
|
|
<label className="flex items-center gap-3 cursor-pointer bg-white px-4 py-3 rounded-xl border border-blue-200 shadow-sm hover:border-blue-400 transition-colors">
|
|
<input type="checkbox" checked={byokEnabled} onChange={e => setByokEnabled(e.target.checked)} className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500" />
|
|
<span className="text-[10px] font-bold text-gray-700">Enable BYOK Mode</span>
|
|
</label>
|
|
{byokEnabled && (
|
|
<input
|
|
type="text"
|
|
placeholder="Enter Custom Quantum Key..."
|
|
value={byokKey}
|
|
onChange={e => setByokKey(e.target.value)}
|
|
className="text-[10px] font-mono bg-white border border-blue-200 rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-6 flex justify-between items-center">
|
|
<input type="text" placeholder="Filter Matrix..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="border rounded-full px-4 py-2 text-sm w-64 outline-none focus:ring-2 focus:ring-blue-500" />
|
|
</div>
|
|
<div className="space-y-8">
|
|
{['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
|
const filtered = systemFeatures.filter(f => (f.module === mod || (!f.module && mod === 'IAM')) && (f.name.toLowerCase().includes(searchQuery.toLowerCase()) || f.key.toLowerCase().includes(searchQuery.toLowerCase())));
|
|
if (filtered.length === 0) return null;
|
|
return (
|
|
<div key={mod}>
|
|
<div className="flex justify-between items-center mb-4 border-l-4 border-blue-600 pl-3">
|
|
<span className="text-[10px] font-black text-blue-600">{mod} MODULES</span>
|
|
<div className="space-x-2">
|
|
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'GRANTED'); setTenantLicenses(u); }} className="text-[8px] font-bold text-emerald-600 bg-emerald-50 px-2 py-1 rounded">Grant All</button>
|
|
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'UPSELL'); setTenantLicenses(u); }} className="text-[8px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded">Upsell All</button>
|
|
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'HIDDEN'); setTenantLicenses(u); }} className="text-[8px] font-bold text-red-600 bg-red-50 px-2 py-1 rounded">Block All</button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{filtered.map(feat => {
|
|
let isIncludedInPackage = false;
|
|
if (selectedTenant?.package) {
|
|
try {
|
|
const pkgFeats = JSON.parse(selectedTenant.package.features || "[]");
|
|
if (pkgFeats.includes(feat.key)) isIncludedInPackage = true;
|
|
} catch(e) {}
|
|
}
|
|
const val = isIncludedInPackage ? 'GRANTED' : (tenantLicenses[feat.key] || 'HIDDEN');
|
|
|
|
return (
|
|
<div key={feat.key} className={`flex justify-between items-center p-3 border rounded-lg ${isIncludedInPackage ? 'bg-gray-50 border-gray-200 opacity-70' : 'hover:border-blue-200'}`}>
|
|
<div className="text-sm font-bold">{feat.name} <span className="text-[9px] font-mono text-gray-400 block">{feat.key}</span></div>
|
|
<div className="flex items-center gap-3">
|
|
{isIncludedInPackage && <span className="text-[9px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 uppercase">Included in Package</span>}
|
|
<select
|
|
value={val}
|
|
onChange={e => setTenantLicenses({...tenantLicenses, [feat.key]: e.target.value})}
|
|
className="text-xs font-bold p-1 rounded border"
|
|
disabled={isIncludedInPackage}
|
|
>
|
|
<option value="GRANTED">🟢 GRANTED</option>
|
|
<option value="UPSELL">💎 UPSELL</option>
|
|
<option value="HIDDEN">❌ HIDDEN</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="p-5 border-t bg-gray-50 flex justify-end gap-3 relative z-50 shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
|
|
<button type="button" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowTenantModal(false); }} disabled={isSavingMatrix} className="px-6 py-2.5 border border-gray-300 rounded-xl hover:bg-gray-100 text-gray-700 font-bold transition-colors cursor-pointer relative z-50">Cancel</button>
|
|
<button type="button" onClick={(e) => { e.preventDefault(); e.stopPropagation(); saveTenantMatrix(); }} disabled={isSavingMatrix} className={`px-6 py-2.5 rounded-xl font-bold transition-all shadow-lg cursor-pointer relative z-50 flex items-center gap-2 ${isSavingMatrix ? 'bg-blue-400 text-white cursor-not-allowed' : 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-blue-500/30 hover:-translate-y-0.5'}`}>
|
|
{isSavingMatrix ? (
|
|
<>
|
|
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
|
Menyimpan...
|
|
</>
|
|
) : 'Deploy Matrix'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SECURITY TIER SWITCH MODAL */}
|
|
{tierModal && (
|
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-md" onClick={() => !tierSwitching && setTierModal(null)}>
|
|
<div className="bg-gradient-to-b from-slate-900 to-slate-950 w-full max-w-lg rounded-2xl shadow-2xl border border-slate-700/50 overflow-hidden" onClick={e => e.stopPropagation()}>
|
|
{/* Header */}
|
|
<div className="p-6 text-center border-b border-slate-800">
|
|
<div className="w-14 h-14 mx-auto mb-3 rounded-2xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center shadow-lg shadow-cyan-500/20">
|
|
<svg className="w-7 h-7 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
</div>
|
|
<h3 className="text-lg font-bold text-white">Security Tier Configuration</h3>
|
|
<p className="text-xs text-slate-400 mt-1">Tenant: <strong className="text-white">{tierModal.tenantName}</strong></p>
|
|
</div>
|
|
|
|
{/* Radio Options */}
|
|
<div className="p-5 space-y-2">
|
|
{Object.entries(TIER_CONFIG).map(([key, cfg]) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => setSelectedTier(key)}
|
|
className={`w-full text-left p-4 rounded-xl border-2 transition-all ${
|
|
selectedTier === key
|
|
? `${cfg.bg} ${cfg.border} ring-1 ${cfg.border}`
|
|
: 'border-slate-700/50 hover:border-slate-600'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
|
selectedTier === key ? cfg.border : 'border-slate-600'
|
|
}`}>
|
|
{selectedTier === key && <div className={`w-2.5 h-2.5 rounded-full ${
|
|
key === 'STANDARD' ? 'bg-emerald-400' : key === 'SOVEREIGN' ? 'bg-cyan-400' : 'bg-amber-400'
|
|
}`}></div>}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-base">{cfg.icon}</span>
|
|
<span className={`text-sm font-bold ${selectedTier === key ? cfg.color : 'text-slate-300'}`}>{cfg.label}</span>
|
|
{tierModal.currentTier === key && <span className="text-[8px] font-bold px-1.5 py-0.5 rounded bg-slate-700 text-slate-400 uppercase">Current</span>}
|
|
</div>
|
|
<p className="text-[11px] text-slate-500 mt-0.5 ml-7">{cfg.desc}</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="px-5 pb-5 flex gap-3">
|
|
<button
|
|
onClick={() => setTierModal(null)}
|
|
disabled={tierSwitching}
|
|
className="flex-1 px-4 py-3 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold rounded-xl transition-colors border border-slate-700"
|
|
>Batal</button>
|
|
<button
|
|
onClick={applySecurityTier}
|
|
disabled={tierSwitching || selectedTier === tierModal.currentTier}
|
|
className={`flex-1 px-4 py-3 font-bold rounded-xl transition-all shadow-lg flex items-center justify-center gap-2 ${
|
|
tierSwitching || selectedTier === tierModal.currentTier
|
|
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
|
|
: 'bg-gradient-to-r from-cyan-600 to-blue-600 text-white hover:shadow-cyan-500/30 hover:scale-[1.02]'
|
|
}`}
|
|
>
|
|
{tierSwitching ? (
|
|
<><div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div> Switching...</>
|
|
) : (
|
|
<><svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> {selectedTier === tierModal.currentTier ? 'No Change' : 'Apply Security Tier'}</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|