larxius commited on
Commit
1167f99
·
verified ·
1 Parent(s): 97266bb

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
- // Convert from cents (stored in DB) to dollars for UI display
10
- const [monthly, setMonthly] = useState((tier.monthly_price / 100).toFixed(2));
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
- // Convert dollars back to cents before sending to API
18
- await onSave(tier.id, Math.round(parseFloat(monthly) * 100), Math.round(parseFloat(yearly) * 100));
 
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-lg flex flex-col gap-md shadow-sm hover:shadow-md transition-shadow relative overflow-hidden group">
26
- {/* Accent Top Border based on tier name */}
27
- <div className={`absolute top-0 left-0 right-0 h-1 ${tier.id === 'free' ? 'bg-surface-container-high' :
28
- tier.id === 'quick' ? 'bg-blue-400' :
29
- tier.id === 'standard' ? 'bg-primary' :
30
- tier.id === 'advanced' ? 'bg-purple-500' : 'bg-orange-500'
31
- }`}></div>
32
-
33
- <div className="flex justify-between items-start">
34
- <div>
35
- <h3 className="font-headline-sm text-[18px] font-bold text-on-surface capitalize flex items-center gap-2">
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-sm">
45
  <div className="flex flex-col gap-1">
46
- <label className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wide">Monthly Price</label>
47
  <div className="relative flex items-center">
48
- <span className="absolute left-3 text-on-surface-variant font-bold">$</span>
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-high border border-outline-variant rounded-lg pl-8 pr-3 py-2 focus:border-primary focus:ring-1 focus:ring-primary outline-none text-on-surface font-mono font-bold transition-all"
56
  />
57
  </div>
58
  </div>
59
 
60
  <div className="flex flex-col gap-1">
61
- <label className="text-[12px] font-bold text-on-surface-variant uppercase tracking-wide">Yearly Price</label>
62
  <div className="relative flex items-center">
63
- <span className="absolute left-3 text-on-surface-variant font-bold">$</span>
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-high border border-outline-variant rounded-lg pl-8 pr-3 py-2 focus:border-primary focus:ring-1 focus:ring-primary outline-none text-on-surface font-mono font-bold transition-all"
71
  />
72
  </div>
73
  </div>
74
  </div>
75
 
76
- <div className="flex justify-end mt-sm pt-md border-t border-outline-variant/50">
77
- <button
78
- onClick={handleSave}
79
- disabled={saving}
80
- className={`px-xl py-2 rounded-lg font-bold text-[13px] transition-all flex items-center gap-2 ${saved
81
- ? 'bg-green-500 text-white'
82
- : 'bg-primary/10 text-primary hover:bg-primary/20 border border-primary/20'
 
 
83
  }`}
84
- >
85
- {saving ? (
86
- <span className="material-symbols-outlined animate-spin text-[16px]">sync</span>
87
- ) : saved ? (
88
- <span className="material-symbols-outlined text-[16px]">check_circle</span>
89
- ) : (
90
- <span className="material-symbols-outlined text-[16px]">save</span>
91
- )}
92
- {saving ? 'Saving...' : saved ? 'Saved!' : 'Save Changes'}
93
- </button>
94
- </div>
 
 
 
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) setTiers(await res.json());
388
- } catch (err) { }
 
 
 
 
 
 
 
 
 
 
 
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