325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
import { useQuery } from "@tanstack/react-query"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Download, Users, FileText, DollarSign, HardDrive, AlertCircle } from "lucide-react"
|
|
import { analyticsService, systemService } from "@/services"
|
|
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
|
|
import { toast } from "sonner"
|
|
|
|
export default function DashboardPage() {
|
|
const { data: overview, isLoading: overviewLoading } = useQuery({
|
|
queryKey: ['admin', 'analytics', 'overview'],
|
|
queryFn: () => analyticsService.getOverview(),
|
|
})
|
|
|
|
const { data: userGrowth, isLoading: growthLoading } = useQuery({
|
|
queryKey: ['admin', 'analytics', 'users', 'growth'],
|
|
queryFn: () => analyticsService.getUserGrowth(30),
|
|
})
|
|
|
|
const { data: revenue, isLoading: revenueLoading } = useQuery({
|
|
queryKey: ['admin', 'analytics', 'revenue'],
|
|
queryFn: () => analyticsService.getRevenue('30days'),
|
|
})
|
|
|
|
const { data: health, isLoading: healthLoading } = useQuery({
|
|
queryKey: ['admin', 'system', 'health'],
|
|
queryFn: () => systemService.getHealth(),
|
|
})
|
|
|
|
const { data: errorRate, isLoading: errorRateLoading } = useQuery({
|
|
queryKey: ['admin', 'analytics', 'error-rate'],
|
|
queryFn: () => analyticsService.getErrorRate(7),
|
|
})
|
|
|
|
const handleExport = () => {
|
|
try {
|
|
// Create CSV content from current dashboard data
|
|
const csvContent = [
|
|
['Metric', 'Value'],
|
|
['Total Users', overview?.users?.total || 0],
|
|
['Active Users', overview?.users?.active || 0],
|
|
['Inactive Users', overview?.users?.inactive || 0],
|
|
['Total Invoices', overview?.invoices?.total || 0],
|
|
['Total Revenue', overview?.revenue?.total || 0],
|
|
['Storage Used', overview?.storage?.totalSize || 0],
|
|
['Total Documents', overview?.storage?.documents || 0],
|
|
['Error Rate', errorRate?.errorRate || 0],
|
|
['Total Errors', errorRate?.errors || 0],
|
|
['Export Date', new Date().toISOString()],
|
|
]
|
|
.map(row => row.join(','))
|
|
.join('\n')
|
|
|
|
// Create and download the file
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
const url = window.URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = `admin-dashboard-${new Date().toISOString().split('T')[0]}.csv`
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
toast.success("Dashboard data exported successfully!")
|
|
} catch (error) {
|
|
toast.error("Failed to export data. Please try again.")
|
|
console.error('Export error:', error)
|
|
}
|
|
}
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
}).format(amount)
|
|
}
|
|
|
|
const formatBytes = (bytes: number) => {
|
|
if (bytes === 0) return '0 Bytes'
|
|
const k = 1024
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-3xl font-bold">Dashboard Overview</h2>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
</div>
|
|
<Button variant="outline" onClick={handleExport}>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Export Data
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
{overviewLoading ? (
|
|
<div className="text-2xl font-bold">...</div>
|
|
) : (
|
|
<>
|
|
<div className="text-2xl font-bold">{overview?.users?.total || 0}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{overview?.users?.active || 0} active, {overview?.users?.inactive || 0} inactive
|
|
</p>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Invoices</CardTitle>
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
{overviewLoading ? (
|
|
<div className="text-2xl font-bold">...</div>
|
|
) : (
|
|
<>
|
|
<div className="text-2xl font-bold">{overview?.invoices?.total || 0}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
All time invoices
|
|
</p>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
{overviewLoading ? (
|
|
<div className="text-2xl font-bold">...</div>
|
|
) : (
|
|
<>
|
|
<div className="text-2xl font-bold">
|
|
{overview?.revenue ? formatCurrency(overview.revenue.total) : '$0.00'}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Total revenue
|
|
</p>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Storage Usage</CardTitle>
|
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
{overviewLoading ? (
|
|
<div className="text-2xl font-bold">...</div>
|
|
) : (
|
|
<>
|
|
<div className="text-2xl font-bold">
|
|
{overview?.storage ? formatBytes(overview.storage.totalSize) : '0 Bytes'}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{overview?.storage?.documents || 0} documents
|
|
</p>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Charts Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* User Growth Chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>User Growth (Last 30 Days)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{growthLoading ? (
|
|
<div className="h-[300px] flex items-center justify-center">Loading...</div>
|
|
) : userGrowth && userGrowth.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={userGrowth}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Line type="monotone" dataKey="total" stroke="#8884d8" name="Total Users" />
|
|
<Line type="monotone" dataKey="admins" stroke="#82ca9d" name="Admins" />
|
|
<Line type="monotone" dataKey="regular" stroke="#ffc658" name="Regular Users" />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
|
No data available
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Revenue Chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Revenue Analytics (Last 30 Days)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{revenueLoading ? (
|
|
<div className="h-[300px] flex items-center justify-center">Loading...</div>
|
|
) : revenue && revenue.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={revenue}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Bar dataKey="revenue" fill="#8884d8" name="Revenue" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
|
No data available
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Error Rate Chart */}
|
|
{errorRate && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5" />
|
|
Error Rate (Last 7 Days)
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{errorRateLoading ? (
|
|
<div className="h-[200px] flex items-center justify-center">Loading...</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Total Errors</p>
|
|
<p className="text-2xl font-bold">{errorRate.errors || 0}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Total Requests</p>
|
|
<p className="text-2xl font-bold">{errorRate.total || 0}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Error Rate</p>
|
|
<p className="text-2xl font-bold">
|
|
{errorRate.errorRate ? `${errorRate.errorRate.toFixed(2)}%` : '0%'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="w-full bg-muted rounded-full h-4">
|
|
<div
|
|
className="bg-destructive h-4 rounded-full transition-all"
|
|
style={{
|
|
width: `${Math.min(errorRate.errorRate || 0, 100)}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* System Health */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertCircle className="h-5 w-5" />
|
|
System Health
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{healthLoading ? (
|
|
<div>Loading system health...</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Status</p>
|
|
<p className="text-lg font-semibold capitalize">{health?.status || 'Unknown'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Database</p>
|
|
<p className="text-lg font-semibold capitalize">{health?.database || 'Unknown'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Recent Errors</p>
|
|
<p className="text-lg font-semibold">{health?.recentErrors || 0}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Active Users</p>
|
|
<p className="text-lg font-semibold">{health?.activeUsers || 0}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|