main
Some checks failed
Deploy Frontend / deploy (push) Has been cancelled

This commit is contained in:
2026-02-05 01:34:13 +03:00
commit 6e3bee17ef
52 changed files with 12306 additions and 0 deletions

396
pages/AdminDashboard.tsx Normal file
View File

@@ -0,0 +1,396 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { Users, PlusCircle, Shield, ArrowLeft, RefreshCw, Key, DollarSign, BarChart2, Save, Activity } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Tooltip } from '../components/Tooltip';
import { Layout } from '../components/Layout';
// Simple SVG Bar Chart Component
const AnalyticsChart = ({ data }: { data: any[] }) => {
if (!data || data.length === 0) return <div className="text-center text-stone-400 py-10">No data available for this period.</div>;
const maxVal = Math.max(...data.map(d => Math.max(d.revenue, d.usage)), 10); // Prevent zero division
const height = 200;
const barWidth = 40;
const gap = 20;
const width = data.length * (barWidth + gap);
return (
<div className="overflow-x-auto pb-4">
<svg width={Math.max(width, 600)} height={height + 50} className="font-mono text-[10px]">
{/* Y-Axis Grid */}
{[0, 0.5, 1].map(t => (
<line key={t} x1={0} y1={height * (1 - t)} x2="100%" y2={height * (1 - t)} stroke="#e5e5e5" strokeDasharray="4" />
))}
{data.map((d, i) => {
const x = i * (barWidth + gap);
const hRev = (d.revenue / maxVal) * height;
const hCost = (d.usage / maxVal) * height;
return (
<g key={i} transform={`translate(${x}, 0)`}>
{/* Revenue Bar (Green) */}
<rect x={0} y={height - hRev} width={barWidth / 2} height={hRev} fill="#22c55e" rx="2" />
{/* Cost Bar (Red) */}
<rect x={barWidth / 2} y={height - hCost} width={barWidth / 2} height={hCost} fill="#ef4444" rx="2" />
{/* Label */}
<text x={barWidth / 2} y={height + 15} textAnchor="middle" fill="#78716c">{d.date.slice(5)}</text>
{/* Values */}
<text x={0} y={height - hRev - 5} className="fill-green-600 font-bold" fontSize="8">${d.revenue.toFixed(1)}</text>
</g>
);
})}
</svg>
<div className="flex gap-4 justify-center mt-4 text-xs font-bold">
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-green-500 rounded"></div> Revenue</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-red-500 rounded"></div> Usage Cost</div>
</div>
</div>
);
};
const AdminDashboard: React.FC = () => {
const { user } = useAuth();
const navigate = useNavigate();
// Tab State
const [activeTab, setActiveTab] = useState<'users' | 'pricing' | 'analytics'>('users');
// Data State
const [users, setUsers] = useState<any[]>([]);
const [configs, setConfigs] = useState<any>({});
const [analytics, setAnalytics] = useState<any[]>([]);
// Loading State
const [loading, setLoading] = useState(false);
const [editingConfig, setEditingConfig] = useState<string | null>(null);
const [configValue, setConfigValue] = useState("");
const [analyticsRange, setAnalyticsRange] = useState("30d");
// NEW: User Management State
const [selectedUser, setSelectedUser] = useState<any>(null);
useEffect(() => {
if (user && user.role !== 'ADMIN') {
navigate('/');
return;
}
fetchData();
}, [user, activeTab, analyticsRange]);
const fetchData = async () => {
setLoading(true);
const token = localStorage.getItem('token');
const headers = { Authorization: `Bearer ${token}` };
try {
if (activeTab === 'users') {
const res = await axios.get('/api/admin/users', { headers });
setUsers(res.data.users);
} else if (activeTab === 'pricing') {
const res = await axios.get('/api/admin/config', { headers });
setConfigs(res.data);
} else if (activeTab === 'analytics') {
const res = await axios.get(`/api/admin/analytics?range=${analyticsRange}`, { headers });
const data = Array.isArray(res.data.analytics) ? res.data.analytics : [];
setAnalytics(data);
}
} catch (err) {
console.error("Fetch Error:", err);
} finally {
setLoading(false);
}
};
const updateConfig = async (key: string) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/config', { key, value: configValue }, {
headers: { Authorization: `Bearer ${token}` }
});
setConfigs({ ...configs, [key]: configValue });
setEditingConfig(null);
} catch (e) {
alert("Failed to update config");
}
};
const addCredits = async (userId: string, amount: number) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/credits', { userId, amount }, {
headers: { Authorization: `Bearer ${token}` }
});
fetchData(); // Refresh list to show new balance
if (selectedUser) {
// Update local selected user state immediately for better UX
setSelectedUser((prev: any) => ({ ...prev, credits: prev.credits + amount }));
}
} catch (err) {
alert("Failed to add credits");
}
};
const updateUserRole = async (userId: string, role: string) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/role', { userId, role }, {
headers: { Authorization: `Bearer ${token}` }
});
fetchData(); // Refresh list
if (selectedUser) {
setSelectedUser((prev: any) => ({ ...prev, role }));
}
} catch (err: any) {
alert(err.response?.data?.error || "Failed to update role");
}
};
return (
<Layout>
<div className="max-w-7xl mx-auto p-6 pt-12 space-y-8">
{/* Admin Sub-Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-8">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/')}
className="p-3 bg-white hover:bg-stone-100 border border-stone-200 rounded-2xl transition-all shadow-sm group"
>
<ArrowLeft className="w-5 h-5 text-stone-600 group-hover:-translate-x-1 transition-transform" />
</button>
<div>
<h1 className="text-3xl font-black tracking-tighter text-stone-900 flex items-center gap-3">
<Shield className="w-8 h-8 text-purple-600" />
Admin Command Center
</h1>
<p className="text-stone-500 font-medium text-sm">System management and financial oversight.</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex bg-stone-200 p-1 rounded-xl">
<button onClick={() => setActiveTab('users')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'users' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Users</button>
<button onClick={() => setActiveTab('pricing')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'pricing' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Pricing</button>
<button onClick={() => setActiveTab('analytics')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'analytics' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Analytics</button>
</div>
<button onClick={fetchData} className="p-3 bg-white hover:bg-stone-50 border border-stone-200 rounded-xl transition-all shadow-sm">
<RefreshCw className={`w-4 h-4 text-stone-600 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* --- USERS TAB --- */}
{activeTab === 'users' && (
<div className="bg-white rounded-xl shadow-sm border border-stone-200 overflow-hidden">
<div className="p-6 border-b border-stone-100 flex items-center gap-3">
<Users className="w-5 h-5 text-stone-400" />
<h3 className="font-bold text-lg">User Management</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-stone-50 border-b border-stone-200">
<tr>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">User</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">Credits</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">P&L (Rev/Cost)</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">API Key</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100">
{users.map(u => (
<tr key={u.id} className="hover:bg-stone-50 transition-colors">
<td className="px-6 py-4">
<div className="font-bold">{u.email}</div>
<div className="text-xs text-stone-400 font-mono">{u.id}</div>
</td>
<td className="px-6 py-4 font-mono font-bold text-stone-700">{u.credits}</td>
<td className="px-6 py-4 font-mono text-xs">
<span className="text-green-600 block">+${(u.totalRevenue || 0).toFixed(2)}</span>
<span className="text-red-500 block">-${(u.totalCost || 0).toFixed(4)}</span>
</td>
<td className="px-6 py-4">
{u.apiKey ? <span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs font-bold">Key Active</span> : <span className="text-stone-400 text-xs">No Key</span>}
</td>
<td className="px-6 py-4">
<button
onClick={() => setSelectedUser(u)}
className="px-3 py-1 bg-purple-600 text-white rounded-lg text-xs font-bold hover:bg-purple-500 shadow-sm flex items-center gap-1"
>
<Key className="w-3 h-3" /> Manage
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* --- USER MANAGEMENT MODAL --- */}
{selectedUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden border border-stone-100">
{/* Header */}
<div className="bg-stone-50 px-6 py-4 border-b border-stone-200 flex justify-between items-center">
<div>
<h3 className="text-lg font-black text-stone-800">Manage User</h3>
<p className="text-xs text-stone-500 font-mono">{selectedUser.email}</p>
</div>
<button
onClick={() => setSelectedUser(null)}
className="p-1 hover:bg-stone-200 rounded-full transition-colors text-stone-400 hover:text-stone-600"
>
</button>
</div>
<div className="p-6 space-y-6">
{/* Role Management */}
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-stone-400">User Role</label>
<div className="flex gap-2">
<select
value={selectedUser.role}
onChange={(e) => updateUserRole(selectedUser.id, e.target.value)}
className="flex-1 bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-bold focus:ring-2 focus:ring-purple-500 outline-none"
>
<option value="USER">USER (Standard)</option>
<option value="VIP">VIP (High Priority)</option>
<option value="MODERATOR">MODERATOR (Team)</option>
<option value="ADMIN">ADMIN (God Mode)</option>
</select>
</div>
<p className="text-[10px] text-stone-400">
Admins have full access. VIPs get priority queue.
</p>
</div>
<hr className="border-stone-100" />
{/* Credit Management */}
<div className="space-y-4">
<div className="flex justify-between items-end">
<label className="text-xs font-black uppercase tracking-widest text-stone-400">Credit Balance</label>
<span className="text-2xl font-black text-stone-800 font-mono">{selectedUser.credits}</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<p className="text-xs font-bold text-green-600 text-center">Add Credits</p>
<div className="flex gap-1">
<button onClick={() => addCredits(selectedUser.id, 50)} className="flex-1 py-2 bg-green-50 text-green-600 rounded-lg text-xs font-bold hover:bg-green-100 transition-colors">+50</button>
<button onClick={() => addCredits(selectedUser.id, 500)} className="flex-1 py-2 bg-green-50 text-green-600 rounded-lg text-xs font-bold hover:bg-green-100 transition-colors">+500</button>
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-bold text-red-500 text-center">Deduct</p>
<div className="flex gap-1">
<button onClick={() => addCredits(selectedUser.id, -50)} className="flex-1 py-2 bg-red-50 text-red-500 rounded-lg text-xs font-bold hover:bg-red-100 transition-colors">-50</button>
<button onClick={() => addCredits(selectedUser.id, -500)} className="flex-1 py-2 bg-red-50 text-red-500 rounded-lg text-xs font-bold hover:bg-red-100 transition-colors">-500</button>
</div>
</div>
</div>
{/* Manual Input */}
<form
onSubmit={(e) => {
e.preventDefault();
const val = Number((e.target as any).amount.value);
if (val) addCredits(selectedUser.id, val);
(e.target as any).reset();
}}
className="flex gap-2 mt-2"
>
<input
name="amount"
type="number"
placeholder="Custom Amount (+/-)"
className="flex-1 bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-mono focus:ring-2 focus:ring-stone-500 outline-none"
/>
<button type="submit" className="px-4 py-2 bg-stone-800 text-white rounded-xl text-xs font-bold hover:bg-stone-700">Apply</button>
</form>
</div>
</div>
</div>
</div>
)}
{/* --- PRICING TAB --- */}
{activeTab === 'pricing' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{Object.entries(configs).map(([key, value]) => (
<div key={key} className="bg-white p-4 rounded-xl border border-stone-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-bold text-sm text-stone-800 break-all">{key}</h4>
<p className="text-xs text-stone-400">System Configuration Key</p>
</div>
<button
onClick={() => { setEditingConfig(key); setConfigValue(String(value)); }}
className="text-blue-500 hover:text-blue-600 text-xs font-bold"
>
Edit
</button>
</div>
{editingConfig === key ? (
<div className="flex gap-2 mt-2">
<input
value={configValue}
onChange={(e) => setConfigValue(e.target.value)}
className="flex-1 bg-stone-50 border border-stone-300 rounded px-2 py-1 text-sm font-mono"
/>
<button onClick={() => updateConfig(key)} className="bg-green-500 text-white px-2 rounded text-xs"><Save className="w-4 h-4" /></button>
<button onClick={() => setEditingConfig(null)} className="bg-stone-300 text-stone-700 px-2 rounded text-xs"></button>
</div>
) : (
<div className="bg-stone-50 p-2 rounded border border-stone-100 font-mono text-xs text-stone-600 break-all">
{String(value)}
</div>
)}
</div>
))}
</div>
)}
{/* --- ANALYTICS TAB --- */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<Activity className="w-6 h-6 text-blue-500" />
<div>
<h3 className="font-bold text-lg">Financial Performance</h3>
<p className="text-sm text-stone-500">Revenue (Stripe/Purchase) vs Cost (Gemini API)</p>
</div>
</div>
<div className="flex bg-stone-100 rounded p-1">
{['24h', '7d', '30d'].map(r => (
<button
key={r}
onClick={() => setAnalyticsRange(r)}
className={`px-3 py-1 text-xs font-bold rounded ${analyticsRange === r ? 'bg-white shadow text-black' : 'text-stone-500'}`}
>
{r}
</button>
))}
</div>
</div>
<AnalyticsChart data={analytics} />
</div>
</div>
)}
</div>
</Layout>
);
};
export default AdminDashboard;