Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from 'react'; | |
| import { Link } from 'react-router-dom'; | |
| import { motion } from 'framer-motion'; | |
| import { HiHeart, HiUserGroup, HiFlag, HiDocumentText, HiArrowRight, HiPlusCircle } from 'react-icons/hi'; | |
| import api from '../../../services/api'; | |
| import { formatCurrency, formatDate } from '../../../utils/helpers'; | |
| function KpiCard({ icon, label, value, sub, color, bg, trend, i }) { | |
| return ( | |
| <motion.div className="dash-kpi" initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.08 }}> | |
| <div className="dash-kpi__icon-wrap" style={{ background: bg, color: color, fontSize: '1.5rem' }}>{icon}</div> | |
| <div className="dash-kpi__body"> | |
| <div className="dash-kpi__value">{value ?? 'β'}</div> | |
| <div className="dash-kpi__label">{label}</div> | |
| {sub && <div style={{ fontSize: '0.72rem', color: '#9CA3AF', marginTop: '0.15rem' }}>{sub}</div>} | |
| {trend && <span className={`dash-kpi__trend dash-kpi__trend--${trend.up ? 'up' : 'down'}`}>{trend.up ? 'β²' : 'βΌ'} {trend.text}</span>} | |
| </div> | |
| </motion.div> | |
| ); | |
| } | |
| export default function AdminOverview() { | |
| const [donStats, setDonStats] = useState(null); | |
| const [volStats, setVolStats] = useState(null); | |
| const [campStats, setCampStats] = useState(null); | |
| const [helpStats, setHelpStats] = useState(null); | |
| const [recentDons, setRecentDons] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| Promise.allSettled([ | |
| api.get('/donations/stats'), | |
| api.get('/volunteers/stats'), | |
| api.get('/campaigns/stats'), | |
| api.get('/help-requests/stats'), | |
| api.get('/donations/recent?limit=5'), | |
| ]).then(([d, v, c, h, rd]) => { | |
| if (d.status === 'fulfilled') setDonStats(d.value); | |
| if (v.status === 'fulfilled') setVolStats(v.value); | |
| if (c.status === 'fulfilled') setCampStats(c.value); | |
| if (h.status === 'fulfilled') setHelpStats(h.value); | |
| if (rd.status === 'fulfilled') setRecentDons(rd.value.donations || []); | |
| }).finally(() => setLoading(false)); | |
| }, []); | |
| const kpis = [ | |
| { | |
| icon: 'π°', label: 'Total Raised', value: donStats ? formatCurrency(donStats.totalAmount) : 'β', | |
| sub: `${donStats?.totalCount || 0} donations β’ Avg ${donStats ? formatCurrency(donStats.averageAmount) : 'β'}`, | |
| color: '#2D6A4F', bg: '#ecfdf5', | |
| trend: donStats?.thisMonthAmount ? { up: true, text: `${formatCurrency(donStats.thisMonthAmount)} this month` } : null, | |
| }, | |
| { | |
| icon: 'π₯', label: 'Volunteers', value: volStats?.total || 'β', | |
| sub: `${volStats?.pending || 0} pending approval`, | |
| color: '#3B82F6', bg: '#eff6ff', | |
| trend: volStats?.active ? { up: true, text: `${volStats.active} active` } : null, | |
| }, | |
| { | |
| icon: 'π’', label: 'Active Campaigns', value: campStats?.active || 'β', | |
| sub: `${campStats?.total || 0} total β’ ${campStats ? formatCurrency(campStats.totalRaised) : 'β'} raised`, | |
| color: '#8B5CF6', bg: '#f5f3ff', | |
| }, | |
| { | |
| icon: 'π', label: 'Help Requests', value: helpStats?.total || 'β', | |
| sub: `${helpStats?.submitted || 0} new β’ ${helpStats?.fulfilled || 0} fulfilled`, | |
| color: '#EF4444', bg: '#fef2f2', | |
| trend: helpStats?.submitted ? { up: false, text: `${helpStats.submitted} needs review` } : null, | |
| }, | |
| ]; | |
| const quickActions = [ | |
| { to: '/dashboard/campaigns', icon: 'π’', label: 'New Campaign', desc: 'Launch a fundraising campaign' }, | |
| { to: '/dashboard/volunteers', icon: 'β ', label: 'Approve Volunteers', desc: `${volStats?.pending || 0} pending` }, | |
| { to: '/dashboard/requests', icon: 'π', label: 'Triage Requests', desc: `${helpStats?.submitted || 0} new requests` }, | |
| { to: '/dashboard/users', icon: 'π€', label: 'Manage Users', desc: 'Update roles & permissions' }, | |
| ]; | |
| return ( | |
| <div> | |
| <div className="dash-page-header"> | |
| <h1>π Admin Overview</h1> | |
| <p>Real-time snapshot of the foundation's operations.</p> | |
| </div> | |
| {/* KPI Grid */} | |
| <div className="dash-kpi-grid"> | |
| {kpis.map((kpi, i) => ( | |
| <KpiCard key={kpi.label} {...kpi} i={i} loading={loading} /> | |
| ))} | |
| </div> | |
| {/* Quick Actions + Recent Donations */} | |
| <div className="dash-grid-2" style={{ marginTop: '1.25rem' }}> | |
| {/* Quick Actions */} | |
| <div className="dash-card"> | |
| <div className="dash-card__title" style={{ marginBottom: '1rem' }}>β‘ Quick Actions</div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}> | |
| {quickActions.map(a => ( | |
| <Link key={a.to} to={a.to} style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.8rem 1rem', background: '#f9fafb', borderRadius: 12, border: '1px solid #f3f4f6', transition: 'background 0.15s' }} | |
| onMouseEnter={e => e.currentTarget.style.background = '#f3f4f6'} | |
| onMouseLeave={e => e.currentTarget.style.background = '#f9fafb'}> | |
| <span style={{ fontSize: '1.3rem' }}>{a.icon}</span> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ fontWeight: 600, color: '#111827', fontSize: '0.9rem' }}>{a.label}</div> | |
| <div style={{ fontSize: '0.75rem', color: '#9CA3AF' }}>{a.desc}</div> | |
| </div> | |
| <HiArrowRight style={{ color: '#d1d5db' }} /> | |
| </Link> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Recent Donations */} | |
| <div className="dash-card"> | |
| <div className="dash-card__header"> | |
| <div className="dash-card__title"><HiHeart /> Recent Donations</div> | |
| <Link to="/dashboard/donations" style={{ fontSize: '0.82rem', color: '#2D6A4F', display: 'flex', alignItems: 'center', gap: 4 }}> | |
| View All <HiArrowRight /> | |
| </Link> | |
| </div> | |
| {loading ? ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | |
| {[1,2,3].map(i => <div key={i} className="dash-skeleton" style={{ height: 40 }} />)} | |
| </div> | |
| ) : recentDons.length === 0 ? ( | |
| <div className="dash-empty"><div className="dash-empty__icon">π³</div><p>No donations yet.</p></div> | |
| ) : ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}> | |
| {recentDons.map((d, i) => ( | |
| <motion.div key={d.id} initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.06 }} | |
| style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.6rem 0.75rem', background: '#f9fafb', borderRadius: 10 }}> | |
| <div style={{ width: 36, height: 36, borderRadius: '50%', background: '#ecfdf5', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1rem', flexShrink: 0 }}>π°</div> | |
| <div style={{ flex: 1, minWidth: 0 }}> | |
| <div style={{ fontWeight: 600, color: '#2D6A4F', fontSize: '0.9rem' }}>{formatCurrency(d.amount)}</div> | |
| <div style={{ fontSize: '0.75rem', color: '#9CA3AF', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.purpose || 'General'}</div> | |
| </div> | |
| <div style={{ fontSize: '0.72rem', color: '#9CA3AF', textAlign: 'right', flexShrink: 0 }}> | |
| {d.createdAt ? new Date(d.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) : 'β'} | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |