288 lines
14 KiB
TypeScript
288 lines
14 KiB
TypeScript
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, Link } from '@inertiajs/react';
|
|
import React from 'react';
|
|
import { PageProps } from '@/types';
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler,
|
|
ArcElement,
|
|
} from 'chart.js';
|
|
import { Line, Bar, Doughnut } from 'react-chartjs-2';
|
|
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
ArcElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
);
|
|
|
|
interface RecentUser {
|
|
id: number;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
status: string;
|
|
avatar_url?: string;
|
|
created_at: string;
|
|
roles: { name: string }[];
|
|
}
|
|
|
|
interface ChartItem {
|
|
label: string;
|
|
value: number;
|
|
}
|
|
|
|
interface DashboardStats {
|
|
totalUsers: number;
|
|
activeUsers: number;
|
|
totalRoles: number;
|
|
recentUsers: RecentUser[];
|
|
charts: {
|
|
userGrowth: ChartItem[];
|
|
activityStats: ChartItem[];
|
|
};
|
|
}
|
|
|
|
interface DashboardProps extends PageProps {
|
|
stats: DashboardStats;
|
|
}
|
|
|
|
function StatCard({ label, value, sub, icon, variant = 'white', delay = '0s' }: {
|
|
label: string;
|
|
value: string | number;
|
|
sub: string;
|
|
icon: React.ReactNode;
|
|
variant?: 'white' | 'dark' | 'gold' | 'teal';
|
|
delay?: string;
|
|
}) {
|
|
const styles = {
|
|
white: 'bg-white text-[#3D4E4B] border border-gray-100',
|
|
dark: 'bg-[#3D4E4B] text-white border border-[#3D4E4B]',
|
|
gold: 'bg-[#D4A017] text-white border border-[#D4A017]',
|
|
teal: 'bg-[#21A59F] text-white border border-[#21A59F]',
|
|
};
|
|
return (
|
|
<div className={`relative rounded-2xl p-6 anim-up shadow-sm hover:-translate-y-0.5 transition-transform duration-200 ${styles[variant]}`} style={{ animationDelay: delay }}>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<p className="text-sm font-semibold opacity-60">{label}</p>
|
|
<p className="text-3xl font-bold tracking-tighter mt-1">{typeof value === 'number' ? value.toLocaleString() : value}</p>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-xl bg-black/5 border border-white/10 flex items-center justify-center shrink-0">
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
<p className="text-sm font-semibold opacity-60">{sub}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Dashboard({ stats }: DashboardProps) {
|
|
const { totalUsers, activeUsers, totalRoles, recentUsers, charts } = stats;
|
|
const inactiveUsers = totalUsers - activeUsers;
|
|
|
|
// User Growth Chart Configuration
|
|
const growthData = {
|
|
labels: charts.userGrowth.map(d => d.label),
|
|
datasets: [
|
|
{
|
|
label: 'New Registrations',
|
|
data: charts.userGrowth.map(d => d.value),
|
|
borderColor: '#D4A017',
|
|
backgroundColor: 'rgba(212, 160, 23, 0.1)',
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#D4A017',
|
|
},
|
|
],
|
|
};
|
|
|
|
// Activity Bar Chart Configuration
|
|
const activityData = {
|
|
labels: charts.activityStats.map(d => d.label),
|
|
datasets: [
|
|
{
|
|
label: 'System Activity',
|
|
data: charts.activityStats.map(d => d.value),
|
|
backgroundColor: '#3D4E4B',
|
|
borderRadius: 8,
|
|
},
|
|
],
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
backgroundColor: '#3D4E4B',
|
|
titleFont: { size: 12, weight: 'bold' as const },
|
|
padding: 12,
|
|
cornerRadius: 12,
|
|
},
|
|
},
|
|
scales: {
|
|
x: { grid: { display: false }, ticks: { font: { size: 10, weight: 'bold' as const }, color: '#9ca3af' } },
|
|
y: { border: { display: false }, ticks: { font: { size: 10, weight: 'bold' as const }, color: '#9ca3af' }, grid: { color: '#f3f4f6' } },
|
|
},
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout>
|
|
<Head title="Dashboard" />
|
|
|
|
<div className="space-y-6">
|
|
{/* Stats row */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard
|
|
label="Total Users" value={totalUsers} sub="All registered accounts"
|
|
variant="gold" delay="0s"
|
|
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>}
|
|
/>
|
|
<StatCard
|
|
label="Active Users" value={activeUsers} sub={`${totalUsers > 0 ? Math.round((activeUsers / totalUsers) * 100) : 0}% of total`}
|
|
variant="dark" delay="0.08s"
|
|
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>}
|
|
/>
|
|
<StatCard
|
|
label="Inactive Users" value={inactiveUsers} sub="Suspended or deactivated"
|
|
variant="teal" delay="0.16s"
|
|
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>}
|
|
/>
|
|
<StatCard
|
|
label="Roles" value={totalRoles} sub="Defined permission sets"
|
|
variant="white" delay="0.24s"
|
|
icon={<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
{/* Growth Chart */}
|
|
<div className="lg:col-span-8 bg-white rounded-2xl border border-gray-100 shadow-sm p-8 anim-up" style={{ animationDelay: '0.32s' }}>
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div>
|
|
<h3 className="text-base font-black text-[#3D4E4B] tracking-tight leading-none">User Growth</h3>
|
|
<p className="text-xs font-bold text-gray-400 mt-2 tracking-tight">Registration metrics over the last 6 months</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-2.5 h-2.5 rounded-full bg-[#D4A017]" />
|
|
<span className="text-[10px] font-black uppercase tracking-widest text-[#D4A017]">Accounts</span>
|
|
</div>
|
|
</div>
|
|
<div className="h-[300px]">
|
|
<Line data={growthData} options={chartOptions as any} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Breakdown Chart */}
|
|
<div className="lg:col-span-4 bg-[#3D4E4B] rounded-2xl p-8 text-white anim-up shadow-lg shadow-[#3D4E4B]/20" style={{ animationDelay: '0.4s' }}>
|
|
<h3 className="text-base font-black tracking-tight leading-none mb-2">Account Integrity</h3>
|
|
<p className="text-xs font-bold text-white/40 tracking-tight mb-8 uppercase tracking-widest">Global Status Registry</p>
|
|
|
|
<div className="flex flex-col items-center">
|
|
<div className="w-48 h-48 mb-8">
|
|
<Doughnut
|
|
data={{
|
|
labels: ['Active', 'Inactive'],
|
|
datasets: [{
|
|
data: [activeUsers, inactiveUsers],
|
|
backgroundColor: ['#D4A017', 'rgba(255,255,255,0.1)'],
|
|
borderWidth: 0,
|
|
cutout: '75%',
|
|
}]
|
|
}}
|
|
options={{
|
|
plugins: { legend: { display: false } },
|
|
maintainAspectRatio: false,
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="w-full space-y-4">
|
|
<div className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-2 h-2 rounded-full bg-[#D4A017]" />
|
|
<span className="text-xs font-bold opacity-60">Verified Active</span>
|
|
</div>
|
|
<span className="text-sm font-black">{activeUsers}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-2 h-2 rounded-full bg-white/20" />
|
|
<span className="text-xs font-bold opacity-60">Restricted/Inactive</span>
|
|
</div>
|
|
<span className="text-sm font-black">{inactiveUsers}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
{/* Activity Logs Bar Chart */}
|
|
<div className="lg:col-span-4 bg-white rounded-2xl border border-gray-100 shadow-sm p-8 anim-up" style={{ animationDelay: '0.48s' }}>
|
|
<h3 className="text-base font-black text-[#3D4E4B] tracking-tight leading-none mb-8">System Pulse</h3>
|
|
<div className="h-[250px]">
|
|
<Bar data={activityData} options={chartOptions as any} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Users Table */}
|
|
<div className="lg:col-span-8 bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden anim-up" style={{ animationDelay: '0.56s' }}>
|
|
<div className="flex items-center justify-between px-8 py-6 border-b border-gray-50">
|
|
<div>
|
|
<h3 className="text-sm font-black text-[#3D4E4B] tracking-tight uppercase tracking-widest">Recent Registry</h3>
|
|
</div>
|
|
<Link href="/users" className="text-[10px] font-black uppercase tracking-widest text-[#D4A017] hover:text-[#B88B14] transition-colors">
|
|
View Full Archive →
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="divide-y divide-gray-50">
|
|
{recentUsers.map(u => {
|
|
const initials = `${u.first_name?.charAt(0) ?? ''}${u.last_name?.charAt(0) ?? ''}`.toUpperCase();
|
|
return (
|
|
<div key={u.id} className="flex items-center justify-between px-8 py-5 hover:bg-gray-50/30 transition-colors group">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-2xl bg-[#3D4E4B] text-white text-[10px] font-black flex items-center justify-center border-2 border-white shadow-lg shadow-[#3D4E4B]/10 overflow-hidden">
|
|
{u.avatar_url ? <img src={u.avatar_url} className="w-full h-full object-cover" alt="" /> : initials}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-black text-[#3D4E4B] tracking-tight">{u.first_name} {u.last_name}</p>
|
|
<p className="text-[10px] font-bold text-gray-300 uppercase tracking-widest mt-1">{u.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-6">
|
|
<span className={`px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${u.status === 'active' ? 'text-emerald-600 border-emerald-100 bg-emerald-50/50' : 'text-gray-400 border-gray-100 bg-gray-50/50'}`}>
|
|
{u.status}
|
|
</span>
|
|
<Link href={route('users.show', u.id)} className="w-8 h-8 rounded-xl bg-gray-50 flex items-center justify-center text-gray-400 hover:bg-[#3D4E4B] hover:text-white transition-all">
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M9 5l7 7-7 7" /></svg>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|