Files
digicraft-fe/pages/AdminDashboard.tsx
Fahri Can Seçer 6e3bee17ef
Some checks failed
Deploy Frontend / deploy (push) Has been cancelled
main
2026-02-05 01:34:13 +03:00

397 lines
23 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;