Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SuperAdminPanel.jsx
Browse files
frontend/src/pages/SuperAdminPanel.jsx
CHANGED
|
@@ -5,93 +5,130 @@ import { CustomModal } from '../components/CustomModal';
|
|
| 5 |
import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye } from 'lucide-react';
|
| 6 |
import toast from 'react-hot-toast';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const BillingTierCard = ({ tier, onSave }) => {
|
| 9 |
-
|
| 10 |
-
const [
|
| 11 |
-
const [yearly, setYearly] = useState((tier.yearly_price / 100).toFixed(2));
|
| 12 |
const [saving, setSaving] = useState(false);
|
| 13 |
const [saved, setSaved] = useState(false);
|
| 14 |
|
| 15 |
const handleSave = async () => {
|
| 16 |
setSaving(true);
|
| 17 |
-
|
| 18 |
-
|
|
|
|
| 19 |
setSaving(false);
|
| 20 |
setSaved(true);
|
| 21 |
setTimeout(() => setSaved(false), 2000);
|
| 22 |
};
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
return (
|
| 25 |
-
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl p-
|
| 26 |
-
{/* Accent Top Border
|
| 27 |
-
<div className={`absolute top-0 left-0 right-0 h-1 ${tier.id
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
{tier.name}
|
| 37 |
-
</h3>
|
| 38 |
-
<p className="text-[12px] font-mono text-on-surface-variant uppercase tracking-widest mt-1 bg-surface-container px-2 py-0.5 rounded inline-block">
|
| 39 |
-
{tier.id}
|
| 40 |
-
</p>
|
| 41 |
-
</div>
|
| 42 |
</div>
|
| 43 |
|
| 44 |
-
<div className="grid grid-cols-2 gap-md mt-
|
| 45 |
<div className="flex flex-col gap-1">
|
| 46 |
-
<label className="text-[
|
| 47 |
<div className="relative flex items-center">
|
| 48 |
-
<span className="absolute left-3 text-on-surface
|
| 49 |
<input
|
| 50 |
type="number"
|
| 51 |
step="0.01"
|
| 52 |
min="0"
|
| 53 |
value={monthly}
|
| 54 |
onChange={e => setMonthly(e.target.value)}
|
| 55 |
-
className="w-full bg-surface-container
|
| 56 |
/>
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
|
| 60 |
<div className="flex flex-col gap-1">
|
| 61 |
-
<label className="text-[
|
| 62 |
<div className="relative flex items-center">
|
| 63 |
-
<span className="absolute left-3 text-on-surface
|
| 64 |
<input
|
| 65 |
type="number"
|
| 66 |
step="0.01"
|
| 67 |
min="0"
|
| 68 |
value={yearly}
|
| 69 |
onChange={e => setYearly(e.target.value)}
|
| 70 |
-
className="w-full bg-surface-container
|
| 71 |
/>
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
|
| 76 |
-
|
| 77 |
-
<
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
}`}
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
| 95 |
</div>
|
| 96 |
);
|
| 97 |
};
|
|
@@ -384,8 +421,19 @@ const SuperAdminPanel = () => {
|
|
| 384 |
setFetchingTiers(true);
|
| 385 |
try {
|
| 386 |
const res = await fetch('/api/billing/tiers', { headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}` } });
|
| 387 |
-
if (res.ok)
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
setFetchingTiers(false);
|
| 390 |
};
|
| 391 |
|
|
|
|
| 5 |
import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye } from 'lucide-react';
|
| 6 |
import toast from 'react-hot-toast';
|
| 7 |
|
| 8 |
+
const DEFAULT_BILLING_TIERS = [
|
| 9 |
+
{ id: 'quick', name: 'Quick Scan', badge: 'QUICK', monthly_price: 499, yearly_price: 4990 },
|
| 10 |
+
{ id: 'advanced', name: 'Advanced Scan', badge: 'ADVANCED', monthly_price: 4499, yearly_price: 44990 },
|
| 11 |
+
{ id: 'deep', name: 'Deep Scan', badge: 'DEEP', monthly_price: 9999, yearly_price: 99990 },
|
| 12 |
+
{ id: 'enterprise', name: 'Custom Solutions', badge: 'ENTERPRISE', monthly_price: 0, yearly_price: 0, isCustom: true }
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
const BillingTierCard = ({ tier, onSave }) => {
|
| 16 |
+
const [monthly, setMonthly] = useState(((tier.monthly_price || 0) / 100).toFixed(2));
|
| 17 |
+
const [yearly, setYearly] = useState(((tier.yearly_price || 0) / 100).toFixed(2));
|
|
|
|
| 18 |
const [saving, setSaving] = useState(false);
|
| 19 |
const [saved, setSaved] = useState(false);
|
| 20 |
|
| 21 |
const handleSave = async () => {
|
| 22 |
setSaving(true);
|
| 23 |
+
if (onSave) {
|
| 24 |
+
await onSave(tier.id, Math.round(parseFloat(monthly || 0) * 100), Math.round(parseFloat(yearly || 0) * 100));
|
| 25 |
+
}
|
| 26 |
setSaving(false);
|
| 27 |
setSaved(true);
|
| 28 |
setTimeout(() => setSaved(false), 2000);
|
| 29 |
};
|
| 30 |
|
| 31 |
+
const getAccentClass = (id) => {
|
| 32 |
+
const key = (id || '').toLowerCase();
|
| 33 |
+
if (key.includes('quick')) return 'bg-blue-500';
|
| 34 |
+
if (key.includes('advanced')) return 'bg-purple-500';
|
| 35 |
+
if (key.includes('deep')) return 'bg-orange-500';
|
| 36 |
+
if (key.includes('enterprise') || key.includes('custom')) return 'bg-orange-500';
|
| 37 |
+
return 'bg-primary';
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
const getBadge = (tier) => {
|
| 41 |
+
if (tier.badge) return tier.badge;
|
| 42 |
+
const key = (tier.id || '').toLowerCase();
|
| 43 |
+
if (key.includes('quick')) return 'QUICK';
|
| 44 |
+
if (key.includes('advanced')) return 'ADVANCED';
|
| 45 |
+
if (key.includes('deep')) return 'DEEP';
|
| 46 |
+
if (key.includes('enterprise') || key.includes('custom')) return 'ENTERPRISE';
|
| 47 |
+
return (tier.id || '').toUpperCase();
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const getDisplayName = (tier) => {
|
| 51 |
+
if (tier.name && tier.name !== tier.id) return tier.name;
|
| 52 |
+
const key = (tier.id || '').toLowerCase();
|
| 53 |
+
if (key.includes('quick')) return 'Quick Scan';
|
| 54 |
+
if (key.includes('advanced')) return 'Advanced Scan';
|
| 55 |
+
if (key.includes('deep')) return 'Deep Scan';
|
| 56 |
+
if (key.includes('enterprise') || key.includes('custom')) return 'Custom Solutions';
|
| 57 |
+
return tier.name || tier.id;
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const isCustom = (tier.id || '').toLowerCase() === 'enterprise' || tier.isCustom;
|
| 61 |
+
|
| 62 |
return (
|
| 63 |
+
<div className="bg-surface-container-lowest border border-outline-variant/80 rounded-2xl p-md flex flex-col gap-sm shadow-sm relative overflow-hidden">
|
| 64 |
+
{/* Accent Top Border */}
|
| 65 |
+
<div className={`absolute top-0 left-0 right-0 h-1.5 ${getAccentClass(tier.id)}`}></div>
|
| 66 |
+
|
| 67 |
+
<div className="flex flex-col items-start gap-0.5 pt-1">
|
| 68 |
+
<h3 className="font-headline-sm text-[17px] font-bold text-on-surface">
|
| 69 |
+
{getDisplayName(tier)}
|
| 70 |
+
</h3>
|
| 71 |
+
<span className="text-[11px] font-mono font-bold text-on-surface-variant uppercase tracking-wider bg-surface-container px-2 py-0.5 rounded">
|
| 72 |
+
{getBadge(tier)}
|
| 73 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</div>
|
| 75 |
|
| 76 |
+
<div className="grid grid-cols-2 gap-md mt-1">
|
| 77 |
<div className="flex flex-col gap-1">
|
| 78 |
+
<label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider">MONTHLY PRICE</label>
|
| 79 |
<div className="relative flex items-center">
|
| 80 |
+
<span className="absolute left-3 text-on-surface font-bold text-sm">$</span>
|
| 81 |
<input
|
| 82 |
type="number"
|
| 83 |
step="0.01"
|
| 84 |
min="0"
|
| 85 |
value={monthly}
|
| 86 |
onChange={e => setMonthly(e.target.value)}
|
| 87 |
+
className="w-full bg-surface-container border border-outline-variant/80 rounded-lg pl-7 pr-3 py-2 focus:border-primary outline-none text-on-surface font-mono font-bold text-sm"
|
| 88 |
/>
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
|
| 92 |
<div className="flex flex-col gap-1">
|
| 93 |
+
<label className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider">YEARLY PRICE</label>
|
| 94 |
<div className="relative flex items-center">
|
| 95 |
+
<span className="absolute left-3 text-on-surface font-bold text-sm">$</span>
|
| 96 |
<input
|
| 97 |
type="number"
|
| 98 |
step="0.01"
|
| 99 |
min="0"
|
| 100 |
value={yearly}
|
| 101 |
onChange={e => setYearly(e.target.value)}
|
| 102 |
+
className="w-full bg-surface-container border border-outline-variant/80 rounded-lg pl-7 pr-3 py-2 focus:border-primary outline-none text-on-surface font-mono font-bold text-sm"
|
| 103 |
/>
|
| 104 |
</div>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
+
{!isCustom ? (
|
| 109 |
+
<div className="flex justify-end mt-1">
|
| 110 |
+
<button
|
| 111 |
+
onClick={handleSave}
|
| 112 |
+
disabled={saving}
|
| 113 |
+
className={`px-3 py-1.5 border rounded-lg font-bold text-xs transition-all flex items-center gap-1.5 cursor-pointer shadow-sm ${
|
| 114 |
+
saved
|
| 115 |
+
? 'bg-green-500 text-white border-green-500'
|
| 116 |
+
: 'border-outline-variant hover:border-primary text-primary bg-surface-container-lowest'
|
| 117 |
}`}
|
| 118 |
+
>
|
| 119 |
+
{saving ? (
|
| 120 |
+
<span className="material-symbols-outlined animate-spin text-[16px]">sync</span>
|
| 121 |
+
) : saved ? (
|
| 122 |
+
<span className="material-symbols-outlined text-[16px]">check_circle</span>
|
| 123 |
+
) : (
|
| 124 |
+
<span className="material-symbols-outlined text-[16px] text-primary">save</span>
|
| 125 |
+
)}
|
| 126 |
+
<span>{saving ? 'Saving...' : saved ? 'Saved!' : 'Save Changes'}</span>
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
) : (
|
| 130 |
+
<div className="h-8"></div>
|
| 131 |
+
)}
|
| 132 |
</div>
|
| 133 |
);
|
| 134 |
};
|
|
|
|
| 421 |
setFetchingTiers(true);
|
| 422 |
try {
|
| 423 |
const res = await fetch('/api/billing/tiers', { headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}` } });
|
| 424 |
+
if (res.ok) {
|
| 425 |
+
const data = await res.json();
|
| 426 |
+
if (Array.isArray(data) && data.length > 0) {
|
| 427 |
+
setTiers(data);
|
| 428 |
+
} else {
|
| 429 |
+
setTiers(DEFAULT_BILLING_TIERS);
|
| 430 |
+
}
|
| 431 |
+
} else {
|
| 432 |
+
setTiers(DEFAULT_BILLING_TIERS);
|
| 433 |
+
}
|
| 434 |
+
} catch (err) {
|
| 435 |
+
setTiers(DEFAULT_BILLING_TIERS);
|
| 436 |
+
}
|
| 437 |
setFetchingTiers(false);
|
| 438 |
};
|
| 439 |
|