451 lines
28 KiB
JavaScript
451 lines
28 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, Link, router } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
Users,
|
|
ShieldCheck,
|
|
CreditCard,
|
|
BadgeDollarSign,
|
|
CheckCircle,
|
|
ArrowRight,
|
|
ArrowUpRight,
|
|
MessageSquare,
|
|
TrendingUp,
|
|
Percent,
|
|
PieChart,
|
|
Sparkles
|
|
} from 'lucide-react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
|
|
export default function Dashboard({ stats, recent_verifications, recent_subscriptions }) {
|
|
const [activeTrendTab, setActiveTrendTab] = useState('All');
|
|
const [selectedSlice, setSelectedSlice] = useState(null);
|
|
|
|
// Dynamic trend monthly data for custom SVG Line chart
|
|
const trendData = stats?.trend_data || [
|
|
{ month: 'Dec', basic: 0, premium: 0, vip: 0 },
|
|
{ month: 'Jan', basic: 0, premium: 0, vip: 0 },
|
|
{ month: 'Feb', basic: 0, premium: 0, vip: 0 },
|
|
{ month: 'Mar', basic: 0, premium: 0, vip: 0 },
|
|
{ month: 'Apr', basic: 0, premium: 0, vip: 0 },
|
|
{ month: 'May', basic: 0, premium: 0, vip: 0 }
|
|
];
|
|
|
|
const maxVal = Math.max(
|
|
10,
|
|
...trendData.map(d => Math.max(d.basic || 0, d.premium || 0, d.vip || 0))
|
|
);
|
|
|
|
// Compute lines points for SVG Spline
|
|
const getCoordinatesForLine = (key, height, width, max) => {
|
|
const points = [];
|
|
const paddingLeft = 35;
|
|
const paddingRight = 15;
|
|
const paddingTop = 20;
|
|
const paddingBottom = 25;
|
|
|
|
const graphHeight = height - paddingTop - paddingBottom;
|
|
const graphWidth = width - paddingLeft - paddingRight;
|
|
|
|
trendData.forEach((d, idx) => {
|
|
const val = d[key] || 0;
|
|
const x = paddingLeft + (idx / (trendData.length - 1)) * graphWidth;
|
|
const y = paddingTop + graphHeight - (val / max) * graphHeight;
|
|
points.push(`${x},${y}`);
|
|
});
|
|
return points.join(' ');
|
|
};
|
|
|
|
return (
|
|
<AdminLayout title="Dashboard">
|
|
<Head title="Dashboard" />
|
|
|
|
{/* Welcome banner */}
|
|
<div className="bg-slate-900 rounded-3xl p-6 text-white mb-8 relative overflow-hidden font-sans shadow-lg shadow-slate-900/10">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-teal-500/10 rounded-full blur-3xl -translate-y-12 translate-x-12" />
|
|
<div className="absolute bottom-0 left-1/3 w-48 h-48 bg-teal-700/15 rounded-full blur-2xl" />
|
|
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center space-x-2 bg-teal-500/20 text-teal-300 px-3 py-1 rounded-full text-xs font-bold w-fit border border-teal-500/20">
|
|
<Sparkles className="w-3.5 h-3.5" />
|
|
<span>System Status: Online</span>
|
|
</div>
|
|
<h2 className="text-2xl font-black tracking-tight mt-2">Welcome back, Administrator</h2>
|
|
<p className="text-sm text-slate-400 font-medium">UAE domestic workers platform is operating optimally. {stats?.verifications_today || 0} automated verifications completed today.</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Link
|
|
href="/admin/analytics"
|
|
className="bg-teal-600 hover:bg-teal-500 text-white font-bold text-xs uppercase tracking-wider px-5 py-3 rounded-xl transition-all shadow-md shadow-teal-600/20 flex items-center gap-1.5"
|
|
>
|
|
<TrendingUp className="w-4 h-4" />
|
|
<span>Full Analytics Hub</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stat Cards Row */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 font-sans">
|
|
{/* Total Workers & active/inactive */}
|
|
<div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Workers Registered</span>
|
|
<div className="w-10 h-10 bg-teal-50 rounded-xl flex items-center justify-center shadow-inner">
|
|
<Users className="w-5 h-5 text-[#0F6E56]" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
|
{stats?.total_workers?.toLocaleString() || 0}
|
|
</h3>
|
|
<div className="flex items-center space-x-2 mt-2 text-xs font-bold">
|
|
<span className="text-emerald-600">{stats?.active_workers || 0} Active</span>
|
|
<span className="text-slate-300">•</span>
|
|
<span className="text-slate-400">{stats?.inactive_workers || 0} Inactive</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Verification Stats */}
|
|
<div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Verification & OCR Rate</span>
|
|
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner">
|
|
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
|
{stats?.verification_rate || 0}%
|
|
</h3>
|
|
<p className="text-xs text-slate-500 font-medium mt-2 flex items-center">
|
|
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 mr-1 inline" />
|
|
<span>{stats?.verified_workers_count?.toLocaleString() || 0} Profiles Auto-OCR Verified</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chat-To-Hire and Hiring Conversion */}
|
|
<div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Hiring Conversion</span>
|
|
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center shadow-inner">
|
|
<Percent className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
|
{stats?.hiring_conversion_rate || 0}%
|
|
</h3>
|
|
<div className="flex items-center space-x-2 mt-2 text-xs font-bold text-blue-600">
|
|
<MessageSquare className="w-3.5 h-3.5" />
|
|
<span>{stats?.chat_to_hire_rate || 0}% Chat-to-Hire</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Subscription Revenue */}
|
|
<div className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-slate-400 uppercase tracking-wider">Monthly Subscription Revenue</span>
|
|
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner">
|
|
<BadgeDollarSign className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<h3 className="text-3xl font-black text-slate-900 tracking-tight">
|
|
AED {stats?.revenue_this_month_aed?.toLocaleString() || 0}
|
|
</h3>
|
|
<p className="text-xs text-emerald-600 font-bold mt-2 flex items-center">
|
|
<ArrowUpRight className="w-3.5 h-3.5 mr-0.5" />
|
|
<span>+{stats?.new_employers_this_week || 0} Employers Added This Week</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Required Analytics and Graphical reports */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8 font-sans">
|
|
{/* 1. Subscription Trends Spline Chart */}
|
|
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 className="text-base font-black text-slate-900 tracking-tight">Subscription Sign-ups Trend</h3>
|
|
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Growth curves across basic, premium & VIP plans</p>
|
|
</div>
|
|
{/* Selector Tabs */}
|
|
<div className="flex bg-slate-100 p-1 rounded-xl">
|
|
{['All', 'Premium', 'VIP'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTrendTab(tab)}
|
|
className={`px-3 py-1 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
|
|
activeTrendTab === tab ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom SVG Line Chart */}
|
|
<div className="relative">
|
|
<svg className="w-full h-56" viewBox="0 0 450 200">
|
|
{/* Grid Lines */}
|
|
<line x1="35" y1="20" x2="435" y2="20" stroke="#f1f5f9" strokeWidth="1" />
|
|
<line x1="35" y1="75" x2="435" y2="75" stroke="#f1f5f9" strokeWidth="1" />
|
|
<line x1="35" y1="130" x2="435" y2="130" stroke="#f1f5f9" strokeWidth="1" />
|
|
<line x1="35" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1" /> {/* Line 1: Basic */}
|
|
{(activeTrendTab === 'All') && (
|
|
<polyline
|
|
fill="none"
|
|
stroke="#94a3b8"
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
points={getCoordinatesForLine('basic', 200, 450, maxVal)}
|
|
className="transition-all duration-500 opacity-60"
|
|
/>
|
|
)}
|
|
|
|
{/* Line 2: Premium */}
|
|
{(activeTrendTab === 'All' || activeTrendTab === 'Premium') && (
|
|
<polyline
|
|
fill="none"
|
|
stroke="#3b82f6"
|
|
strokeWidth="4"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
points={getCoordinatesForLine('premium', 200, 450, maxVal)}
|
|
className="transition-all duration-500"
|
|
/>
|
|
)}
|
|
|
|
{/* Line 3: VIP */}
|
|
{(activeTrendTab === 'All' || activeTrendTab === 'VIP') && (
|
|
<polyline
|
|
fill="none"
|
|
stroke="#10b981"
|
|
strokeWidth="4.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
points={getCoordinatesForLine('vip', 200, 450, maxVal)}
|
|
className="transition-all duration-500"
|
|
/>
|
|
)}
|
|
|
|
{/* X-Axis Labels */}
|
|
{trendData.map((d, i) => {
|
|
const x = 35 + (i / 5) * 385;
|
|
return (
|
|
<text key={i} x={x} y="195" textAnchor="middle" className="text-[10px] font-bold fill-slate-400 font-mono">
|
|
{d.month}
|
|
</text>
|
|
);
|
|
})}
|
|
|
|
{/* Y-Axis Labels */}
|
|
<text x="25" y="25" textAnchor="end" className="text-[10px] font-black fill-slate-300">{maxVal}</text>
|
|
<text x="25" y="80" textAnchor="end" className="text-[10px] font-black fill-slate-300">{Math.round(maxVal / 2)}</text>
|
|
<text x="25" y="135" textAnchor="end" className="text-[10px] font-black fill-slate-300">{Math.round(maxVal / 4)}</text>
|
|
<text x="25" y="185" textAnchor="end" className="text-[10px] font-black fill-slate-300">0</text>
|
|
</svg>
|
|
|
|
{/* Custom Legend */}
|
|
<div className="flex items-center justify-center space-x-6 mt-4">
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3 h-1 bg-slate-400 rounded" />
|
|
<span>Basic Search ({trendData[trendData.length - 1]?.basic || 0})</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3 h-1 bg-blue-500 rounded" />
|
|
<span>Premium Pass ({trendData[trendData.length - 1]?.premium || 0})</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3 h-1 bg-emerald-500 rounded" />
|
|
<span>VIP Concierge ({trendData[trendData.length - 1]?.vip || 0})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 2. Worker Availability Report Donut Chart */}
|
|
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm flex flex-col justify-between">
|
|
<div>
|
|
<h3 className="text-base font-black text-slate-900 tracking-tight flex items-center gap-1.5">
|
|
<PieChart className="w-4 h-4 text-[#0F6E56]" />
|
|
<span>Worker Availability Status Reports</span>
|
|
</h3>
|
|
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Real-time availability of candidates pool</p>
|
|
</div>
|
|
|
|
{(() => {
|
|
const activeVal = stats?.worker_availability?.['Active'] || 0;
|
|
const hiddenVal = stats?.worker_availability?.['Hidden'] || 0;
|
|
const hiredVal = stats?.worker_availability?.['Hired'] || 0;
|
|
const totalVal = activeVal + hiddenVal + hiredVal;
|
|
|
|
const activePct = totalVal > 0 ? Math.round((activeVal / totalVal) * 100) : 0;
|
|
const hiddenPct = totalVal > 0 ? Math.round((hiddenVal / totalVal) * 100) : 0;
|
|
const hiredPct = totalVal > 0 ? Math.round((hiredVal / totalVal) * 100) : 0;
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center my-4">
|
|
{/* Custom Donut Chart */}
|
|
<div className="flex justify-center relative">
|
|
<svg className="w-40 h-40" viewBox="0 0 100 100">
|
|
{totalVal > 0 ? (
|
|
<g className="transform -rotate-90 origin-center">
|
|
{/* Segment 1: Active */}
|
|
<circle
|
|
cx="50" cy="50" r="40"
|
|
fill="transparent"
|
|
stroke="#10b981"
|
|
strokeWidth="12"
|
|
pathLength="100"
|
|
strokeDasharray={`${activePct} ${100 - activePct}`}
|
|
strokeDashoffset="0"
|
|
onMouseEnter={() => setSelectedSlice('Active')}
|
|
onMouseLeave={() => setSelectedSlice(null)}
|
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
|
/>
|
|
{/* Segment 2: Hidden */}
|
|
<circle
|
|
cx="50" cy="50" r="40"
|
|
fill="transparent"
|
|
stroke="#94a3b8"
|
|
strokeWidth="12"
|
|
pathLength="100"
|
|
strokeDasharray={`${hiddenPct} ${100 - hiddenPct}`}
|
|
strokeDashoffset={`-${activePct}`}
|
|
onMouseEnter={() => setSelectedSlice('Hidden')}
|
|
onMouseLeave={() => setSelectedSlice(null)}
|
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
|
/>
|
|
{/* Segment 3: Hired */}
|
|
<circle
|
|
cx="50" cy="50" r="40"
|
|
fill="transparent"
|
|
stroke="#3b82f6"
|
|
strokeWidth="12"
|
|
pathLength="100"
|
|
strokeDasharray={`${hiredPct} ${100 - hiredPct}`}
|
|
strokeDashoffset={`-${activePct + hiddenPct}`}
|
|
onMouseEnter={() => setSelectedSlice('Hired')}
|
|
onMouseLeave={() => setSelectedSlice(null)}
|
|
className="cursor-pointer transition-all duration-300 hover:stroke-[14]"
|
|
/>
|
|
</g>
|
|
) : (
|
|
/* Empty State Grey Circle */
|
|
<circle
|
|
cx="50" cy="50" r="40"
|
|
fill="transparent"
|
|
stroke="#e2e8f0"
|
|
strokeWidth="12"
|
|
/>
|
|
)}
|
|
|
|
{/* Inner Text */}
|
|
<circle cx="50" cy="50" r="28" fill="white" />
|
|
<text x="50" y="47" textAnchor="middle" className="text-[10px] font-black fill-slate-800">
|
|
{selectedSlice || 'TOTAL'}
|
|
</text>
|
|
<text x="50" y="60" textAnchor="middle" className="text-[8px] font-black fill-slate-400 tracking-wider">
|
|
{selectedSlice === 'Active' ? `${activeVal} Workers` : selectedSlice === 'Hidden' ? `${hiddenVal} Workers` : selectedSlice === 'Hired' ? `${hiredVal} Workers` : `${totalVal} Total`}
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
|
|
{/* Interactive legends */}
|
|
<div className="space-y-3">
|
|
<div className="p-3 bg-emerald-50/50 border border-emerald-100 rounded-xl flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-2.5 h-2.5 bg-emerald-500 rounded-full" />
|
|
<span className="text-xs font-bold text-slate-700">Active</span>
|
|
</div>
|
|
<span className="text-xs font-black text-slate-900">{activeVal} ({activePct}%)</span>
|
|
</div>
|
|
<div className="p-3 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-2.5 h-2.5 bg-slate-400 rounded-full" />
|
|
<span className="text-xs font-bold text-slate-700">Hidden</span>
|
|
</div>
|
|
<span className="text-xs font-black text-slate-900">{hiddenVal} ({hiddenPct}%)</span>
|
|
</div>
|
|
<div className="p-3 bg-blue-50/50 border border-blue-100 rounded-xl flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-2.5 h-2.5 bg-blue-500 rounded-full" />
|
|
<span className="text-xs font-bold text-slate-700">Hired</span>
|
|
</div>
|
|
<span className="text-xs font-black text-slate-900">{hiredVal} ({hiredPct}%)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 3. Hiring Funnel & Chat-To-Hire Conversion */}
|
|
<div className="bg-white rounded-3xl p-6 border border-slate-200 shadow-sm mb-8 font-sans">
|
|
<div className="mb-6">
|
|
<h3 className="text-base font-black text-slate-900 tracking-tight">Hiring Conversion & Chat-to-Hire funnel</h3>
|
|
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Platform-wide worker placement workflow stats</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
{(() => {
|
|
const funnelData = stats?.funnel || {
|
|
profiles_browsed: 0,
|
|
chats_initiated: 0,
|
|
candidates_shortlisted: 0,
|
|
workers_hired: 0
|
|
};
|
|
|
|
const browsed = funnelData.profiles_browsed || 0;
|
|
const chats = funnelData.chats_initiated || 0;
|
|
const shortlisted = funnelData.candidates_shortlisted || 0;
|
|
const hired = funnelData.workers_hired || 0;
|
|
|
|
const funnelSteps = [
|
|
{ stage: 'Profiles Browsed', val: browsed.toLocaleString(), pct: '100%', bg: 'bg-slate-100 border-slate-200 text-slate-600' },
|
|
{ stage: 'Chats Initiated', val: chats.toLocaleString(), pct: browsed > 0 ? `${((chats / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-teal-50 border-teal-100 text-teal-700' },
|
|
{ stage: 'Candidates Shortlisted', val: shortlisted.toLocaleString(), pct: browsed > 0 ? `${((shortlisted / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-blue-50 border-blue-100 text-blue-700' },
|
|
{ stage: 'Workers Hired', val: hired.toLocaleString(), pct: browsed > 0 ? `${((hired / browsed) * 100).toFixed(1)}%` : '0%', bg: 'bg-emerald-50 border-emerald-100 text-emerald-700' }
|
|
];
|
|
|
|
return funnelSteps.map((step, i) => (
|
|
<div key={i} className={`p-4 border rounded-2xl ${step.bg} flex flex-col justify-between relative`}>
|
|
{i < 3 && (
|
|
<div className="hidden md:block absolute -right-3 top-1/2 -translate-y-1/2 z-10 w-6 h-6 bg-white border border-slate-100 rounded-full flex items-center justify-center shadow-sm">
|
|
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
|
|
</div>
|
|
)}
|
|
<div>
|
|
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 block mb-1">Step 0{i+1}</span>
|
|
<h4 className="text-xs font-black tracking-tight">{step.stage}</h4>
|
|
</div>
|
|
<div className="mt-4">
|
|
<div className="text-xl font-black tracking-tight">{step.val}</div>
|
|
<div className="text-[10px] font-bold mt-0.5">Ratio: {step.pct}</div>
|
|
</div>
|
|
</div>
|
|
));
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
</AdminLayout>
|
|
);
|
|
}
|