Spaces:
Sleeping
Sleeping
File size: 7,734 Bytes
f4854a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 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>
);
}
|