Veriflo-Dashboard/src/pages/Analytics.jsx

187 lines
9.0 KiB
JavaScript

import { useState, useEffect } from 'react'
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
import { Download, RefreshCw, AlertCircle, Loader2 } from 'lucide-react'
import api from '../services/api'
export default function Analytics() {
const [data, setData] = useState(null)
const [logs, setLogs] = useState([])
const [pagination, setPagination] = useState({ total: 0, page: 1, page_size: 20, total_pages: 1, has_prev: false, has_next: false })
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData()
}, [page, pageSize])
const fetchData = async () => {
setLoading(true)
try {
const [summaryRes, logsRes] = await Promise.all([
api.get('/api/user/analytics/summary'),
api.get(`/api/user/analytics/logs?page=${page}&page_size=${pageSize}`)
])
setData(summaryRes.data)
setLogs(logsRes.data.logs)
setPagination(logsRes.data.pagination || { total: 0, page: 1, page_size: pageSize, total_pages: 1, has_prev: false, has_next: false })
} catch (err) {
console.error("Failed to fetch analytics", err)
} finally {
setLoading(false)
}
}
if (loading || !data) {
return (
<div className="flex justify-center items-center min-h-[50vh]">
<Loader2 className="animate-spin text-accent" size={32} />
</div>
)
}
return (
<div className="flex flex-col gap-8">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-text-primary tracking-tight m-0 mb-1">Usage & Analytics</h2>
<p className="text-text-secondary text-sm font-semibold m-0">
Monitor API performance and OTP delivery rates over time.
</p>
</div>
<div className="flex gap-3">
<button onClick={fetchData} className="neu-btn px-4 py-2 gap-2 flex items-center">
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} /> Refresh
</button>
<button className="neu-btn px-4 py-2 gap-2 text-text-primary flex items-center hover:text-accent">
<Download size={16} /> Export CSV
</button>
</div>
</div>
{/* Main Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* OTPs Sent Line Chart */}
<div className="neu-raised rounded-2xl p-6 lg:p-8">
<h3 className="text-base font-black text-text-primary mb-6">Volume Sent (Last 7 Days)</h3>
<div className="h-[280px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data.chart_data} margin={{ top: 5, right: 0, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--color-shadow-dark)" opacity={0.2} />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fill: 'var(--color-text-muted)', fontSize: 12, fontWeight: 700 }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: 'var(--color-text-muted)', fontSize: 12, fontWeight: 700 }} />
<Tooltip
contentStyle={{ backgroundColor: 'var(--color-base)', borderRadius: '12px', border: 'none', boxShadow: '8px 8px 16px var(--color-shadow-dark)' }}
itemStyle={{ fontWeight: 800 }}
labelStyle={{ color: 'var(--color-text-muted)', marginBottom: '4px', fontWeight: 700, fontSize: '12px' }}
/>
<Line type="monotone" name="Total Sent" dataKey="total" stroke="var(--color-accent)" strokeWidth={4} dot={{ r: 4, fill: 'var(--color-accent)', strokeWidth: 2, stroke: 'var(--color-base)' }} activeDot={{ r: 6, stroke: 'var(--color-base)', strokeWidth: 3 }} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Success vs Failed Bar Chart */}
<div className="neu-raised rounded-2xl p-6 lg:p-8">
<h3 className="text-base font-black text-text-primary mb-6">Delivery Status Breakdown</h3>
<div className="h-[280px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.chart_data} margin={{ top: 5, right: 0, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--color-shadow-dark)" opacity={0.2} />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fill: 'var(--color-text-muted)', fontSize: 12, fontWeight: 700 }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: 'var(--color-text-muted)', fontSize: 12, fontWeight: 700 }} />
<Tooltip
cursor={{ fill: 'rgba(0,0,0,0.03)' }}
contentStyle={{ backgroundColor: 'var(--color-base)', borderRadius: '12px', border: 'none', boxShadow: '8px 8px 16px var(--color-shadow-dark)' }}
itemStyle={{ fontWeight: 800 }}
/>
<Legend iconType="circle" wrapperStyle={{ fontSize: '12px', fontWeight: 700, paddingTop: '10px' }} />
<Bar dataKey="delivered" name="Delivered" stackId="a" fill="var(--color-accent)" radius={[0, 0, 4, 4]} />
<Bar dataKey="failed" name="Failed" stackId="a" fill="#ef4444" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Detailed Log Table */}
<div className="neu-raised rounded-2xl p-6 lg:p-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 mb-6">
<h3 className="text-base font-black text-text-primary m-0">Recent Request Logs</h3>
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-text-muted uppercase tracking-wider">Rows</span>
<select
value={pageSize}
onChange={(e) => {
setPage(1)
setPageSize(Number(e.target.value))
}}
className="neu-input !w-[92px] !py-1.5 !px-2 text-xs"
>
{[10, 20, 50].map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="grid grid-cols-12 px-4 py-2 text-xs font-bold text-text-muted uppercase tracking-widest border-b border-white/5">
<div className="col-span-3">Request ID</div>
<div className="col-span-3">Phone Number</div>
<div className="col-span-3">Timestamp</div>
<div className="col-span-3 text-right">Status</div>
</div>
{logs.length === 0 ? (
<div className="text-center py-10 text-text-muted font-bold">No requests found yet.</div>
) : (
logs.map((log) => (
<div key={log.request_id} className="grid grid-cols-12 items-center px-4 py-3 neu-inset-sm rounded-xl bg-base text-sm font-semibold text-text-secondary">
<div className="col-span-3 font-mono text-xs">{log.request_id}</div>
<div className="col-span-3 text-text-primary">{log.phone}</div>
<div className="col-span-3 text-xs">{new Date(log.created_at).toLocaleString()}</div>
<div className="col-span-3 flex justify-end">
<span className={log.status === 'failed' ? 'badge-error' : 'badge-success'}>
{log.status === 'failed' ? <AlertCircle size={12} /> : null}
{log.status}
</span>
</div>
</div>
))
)}
</div>
<div className="mt-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="text-xs font-semibold text-text-secondary">
Showing page <span className="text-text-primary font-black">{pagination.page}</span> of <span className="text-text-primary font-black">{pagination.total_pages}</span> Total logs: <span className="text-text-primary font-black">{pagination.total}</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setPage((current) => Math.max(1, current - 1))}
disabled={!pagination.has_prev || loading}
className="neu-btn px-3 py-1.5 text-xs disabled:opacity-40"
>
Previous
</button>
<button
type="button"
onClick={() => setPage((current) => current + 1)}
disabled={!pagination.has_next || loading}
className="neu-btn px-3 py-1.5 text-xs disabled:opacity-40"
>
Next
</button>
</div>
</div>
</div>
</div>
)
}