forge/frontend/app/admin/reports/page.tsx
DJP 7a804e896d Initial commit - FORGE AI unified platform
Features:
- Image generation (OpenAI, Gemini, Leonardo, Bria, Stability, Flux)
- Nano Banana iterative editing
- Video generation and upscaling
- Audio TTS, STT, sound effects (ElevenLabs)
- Text prompt studio and alt text
- User authentication with JWT/cookies
- Admin panel with voice management
- Job queue with Celery
- PostgreSQL + Redis backend
- Next.js 15 + FastAPI architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-09 20:39:00 -05:00

326 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import {
TrendingUp,
Download,
Calendar,
BarChart3,
PieChart,
Activity,
} from 'lucide-react';
import AdminGuard from '@/components/AdminGuard';
import api from '@/lib/api';
interface UsageData {
date: string;
jobs: number;
cost: number;
}
interface ModuleUsage {
module: string;
count: number;
percentage: number;
}
interface UserUsage {
user_id: string;
user_email: string;
job_count: number;
total_cost: number;
}
export default function ReportsPage() {
const [dateRange, setDateRange] = useState('7d');
const [loading, setLoading] = useState(true);
const [usageOverTime, setUsageOverTime] = useState<UsageData[]>([]);
const [moduleBreakdown, setModuleBreakdown] = useState<ModuleUsage[]>([]);
const [topUsers, setTopUsers] = useState<UserUsage[]>([]);
const [totals, setTotals] = useState({
totalJobs: 0,
totalCost: 0,
avgJobsPerDay: 0,
});
useEffect(() => {
fetchReportData();
}, [dateRange]);
const fetchReportData = async () => {
setLoading(true);
try {
const response = await api.get('/admin/reports', {
params: { range: dateRange },
});
// Set real data from API
setUsageOverTime(response.data.usage_over_time || []);
setModuleBreakdown(response.data.module_breakdown || []);
setTopUsers(response.data.top_users || []);
setTotals(response.data.totals || {});
} catch (err) {
// Use mock data for demo
setUsageOverTime([
{ date: '2024-12-03', jobs: 45, cost: 12.50 },
{ date: '2024-12-04', jobs: 62, cost: 18.30 },
{ date: '2024-12-05', jobs: 38, cost: 9.80 },
{ date: '2024-12-06', jobs: 71, cost: 22.40 },
{ date: '2024-12-07', jobs: 55, cost: 15.60 },
{ date: '2024-12-08', jobs: 48, cost: 13.20 },
{ date: '2024-12-09', jobs: 47, cost: 14.70 },
]);
setModuleBreakdown([
{ module: 'Image Generation', count: 156, percentage: 35 },
{ module: 'Video Generation', count: 89, percentage: 20 },
{ module: 'Text to Speech', count: 78, percentage: 18 },
{ module: 'Voice to Text', count: 67, percentage: 15 },
{ module: 'Image Upscaling', count: 45, percentage: 10 },
{ module: 'Other', count: 11, percentage: 2 },
]);
setTopUsers([
{ user_id: '1', user_email: 'john@example.com', job_count: 89, total_cost: 28.50 },
{ user_id: '2', user_email: 'jane@example.com', job_count: 67, total_cost: 21.30 },
{ user_id: '3', user_email: 'bob@example.com', job_count: 45, total_cost: 15.80 },
{ user_id: '4', user_email: 'alice@example.com', job_count: 34, total_cost: 12.40 },
{ user_id: '5', user_email: 'admin@forgeai.dev', job_count: 28, total_cost: 9.20 },
]);
setTotals({
totalJobs: 366,
totalCost: 106.50,
avgJobsPerDay: 52.3,
});
} finally {
setLoading(false);
}
};
const handleExport = async (format: 'csv' | 'json') => {
try {
const response = await api.get('/admin/reports/export', {
params: { range: dateRange, format },
responseType: 'blob',
});
const url = window.URL.createObjectURL(response.data);
const a = document.createElement('a');
a.href = url;
a.download = `forge-ai-report-${dateRange}.${format}`;
a.click();
window.URL.revokeObjectURL(url);
toast.success('Report exported!');
} catch (err) {
toast.error('Failed to export report');
}
};
const maxJobs = Math.max(...usageOverTime.map((d) => d.jobs), 1);
return (
<AdminGuard>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-forge-yellow/10 rounded-lg flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-forge-yellow" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Usage Reports</h1>
<p className="text-gray-500">Analytics and usage statistics</p>
</div>
</div>
<div className="flex items-center gap-3">
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="select-field"
>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="365d">Last year</option>
</select>
<button
onClick={() => handleExport('csv')}
className="btn-secondary flex items-center gap-2"
>
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
<div className="flex items-center gap-3 mb-2">
<Activity className="w-5 h-5 text-forge-yellow" />
<span className="text-gray-500">Total Jobs</span>
</div>
<p className="text-3xl font-bold text-white">{totals.totalJobs}</p>
<p className="text-sm text-gray-500 mt-1">
Avg {totals.avgJobsPerDay.toFixed(1)}/day
</p>
</div>
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
<div className="flex items-center gap-3 mb-2">
<BarChart3 className="w-5 h-5 text-green-400" />
<span className="text-gray-500">Estimated Cost</span>
</div>
<p className="text-3xl font-bold text-white">
${totals.totalCost.toFixed(2)}
</p>
<p className="text-sm text-gray-500 mt-1">API usage costs</p>
</div>
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
<div className="flex items-center gap-3 mb-2">
<Calendar className="w-5 h-5 text-blue-400" />
<span className="text-gray-500">Period</span>
</div>
<p className="text-3xl font-bold text-white">
{dateRange === '7d'
? '7 Days'
: dateRange === '30d'
? '30 Days'
: dateRange === '90d'
? '90 Days'
: '1 Year'}
</p>
<p className="text-sm text-gray-500 mt-1">Date range</p>
</div>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Usage Over Time */}
<div className="bg-forge-dark rounded-xl border border-gray-800 p-6">
<h3 className="text-lg font-semibold text-white mb-4">
Jobs Over Time
</h3>
{loading ? (
<div className="h-64 flex items-center justify-center text-gray-500">
Loading...
</div>
) : (
<div className="h-64 flex items-end gap-2">
{usageOverTime.map((data, i) => (
<div
key={i}
className="flex-1 flex flex-col items-center gap-2"
>
<div
className="w-full bg-forge-yellow rounded-t transition-all"
style={{
height: `${(data.jobs / maxJobs) * 200}px`,
minHeight: '4px',
}}
/>
<span className="text-xs text-gray-500">
{new Date(data.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})}
</span>
</div>
))}
</div>
)}
</div>
{/* Module Breakdown */}
<div className="bg-forge-dark rounded-xl border border-gray-800 p-6">
<h3 className="text-lg font-semibold text-white mb-4">
Usage by Module
</h3>
{loading ? (
<div className="h-64 flex items-center justify-center text-gray-500">
Loading...
</div>
) : (
<div className="space-y-4">
{moduleBreakdown.map((module) => (
<div key={module.module}>
<div className="flex items-center justify-between mb-1">
<span className="text-gray-300">{module.module}</span>
<span className="text-gray-500 text-sm">
{module.count} ({module.percentage}%)
</span>
</div>
<div className="progress-bar">
<div
className="progress-bar-fill"
style={{ width: `${module.percentage}%` }}
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Top Users */}
<div className="bg-forge-dark rounded-xl border border-gray-800">
<div className="p-6 border-b border-gray-800">
<h3 className="text-lg font-semibold text-white">Top Users</h3>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">Loading...</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
Rank
</th>
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
User
</th>
<th className="text-right px-6 py-4 text-sm font-medium text-gray-500">
Jobs
</th>
<th className="text-right px-6 py-4 text-sm font-medium text-gray-500">
Est. Cost
</th>
</tr>
</thead>
<tbody>
{topUsers.map((user, index) => (
<tr
key={user.user_id}
className="border-b border-gray-800 last:border-0"
>
<td className="px-6 py-4">
<span
className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-medium ${
index === 0
? 'bg-forge-yellow text-black'
: index === 1
? 'bg-gray-400 text-black'
: index === 2
? 'bg-orange-600 text-white'
: 'bg-forge-gray text-gray-400'
}`}
>
{index + 1}
</span>
</td>
<td className="px-6 py-4 text-white">{user.user_email}</td>
<td className="px-6 py-4 text-right text-gray-300">
{user.job_count}
</td>
<td className="px-6 py-4 text-right text-gray-300">
${user.total_cost.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</AdminGuard>
);
}