Yaltopia-Ticket-Admin/src/pages/admin/dashboard/index.tsx

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>
)
}