Files
biiproject-kit-v2/resources/js/Pages/Dashboard.tsx
T

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