324 lines
19 KiB
JavaScript
324 lines
19 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
Users,
|
|
CreditCard,
|
|
Percent,
|
|
Award,
|
|
Sparkles,
|
|
Calendar,
|
|
Download
|
|
} from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
export default function AnalyticsHub({ user_growth, revenue_breakdown, retention_rates, hiring_funnel }) {
|
|
const [selectedTab, setSelectedTab] = useState('Growth');
|
|
|
|
// Helper coordinates calculation for Cohort growth Area Chart (450w, 200h)
|
|
const getCoordinatesForArea = (key, maxVal) => {
|
|
const points = [];
|
|
const paddingLeft = 40;
|
|
const paddingRight = 15;
|
|
const paddingTop = 20;
|
|
const paddingBottom = 25;
|
|
|
|
const graphHeight = 200 - paddingTop - paddingBottom;
|
|
const graphWidth = 450 - paddingLeft - paddingRight;
|
|
|
|
user_growth.forEach((d, idx) => {
|
|
const val = d[key];
|
|
const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth;
|
|
const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
|
|
points.push(`${x},${y}`);
|
|
});
|
|
|
|
// Add bottom-right and bottom-left to close the area shape
|
|
const startX = paddingLeft;
|
|
const endX = paddingLeft + graphWidth;
|
|
const bottomY = paddingTop + graphHeight;
|
|
|
|
return `${startX},${bottomY} ${points.join(' ')} ${endX},${bottomY}`;
|
|
};
|
|
|
|
const getLineCoordinates = (key, maxVal) => {
|
|
const points = [];
|
|
const paddingLeft = 40;
|
|
const paddingRight = 15;
|
|
const paddingTop = 20;
|
|
const paddingBottom = 25;
|
|
|
|
const graphHeight = 200 - paddingTop - paddingBottom;
|
|
const graphWidth = 450 - paddingLeft - paddingRight;
|
|
|
|
user_growth.forEach((d, idx) => {
|
|
const val = d[key];
|
|
const x = paddingLeft + (idx / (user_growth.length - 1)) * graphWidth;
|
|
const y = paddingTop + graphHeight - (val / maxVal) * graphHeight;
|
|
points.push(`${x},${y}`);
|
|
});
|
|
return points.join(' ');
|
|
};
|
|
|
|
return (
|
|
<AdminLayout title="Reports & Analytics Hub">
|
|
<Head title="System Analytics" />
|
|
|
|
<div className="font-sans max-w-7xl mx-auto space-y-8">
|
|
|
|
{/* Title and date picker */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Interactive Platform Reports & Analytics Hub</h1>
|
|
<p className="text-xs text-gray-500 mt-0.5">Explore subscription cohort retention, job conversions ratios, availability trends, and monthly billing splits.</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200">
|
|
{['Growth', 'Revenue', 'Funnel'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
onClick={() => setSelectedTab(tab)}
|
|
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all ${
|
|
selectedTab === tab
|
|
? 'bg-white text-[#0F6E56] shadow-sm'
|
|
: 'text-slate-600 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{tab} Report
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button className="inline-flex items-center px-4 py-2.5 bg-slate-950 text-white rounded-xl text-xs font-bold hover:bg-slate-900 transition-colors shadow-sm space-x-2 uppercase tracking-wider">
|
|
<Download className="w-4 h-4" />
|
|
<span>Export PDF</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Analytical Charts grid depending on tabs */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
|
|
{/* Growth cohort Report */}
|
|
{selectedTab === 'Growth' && (
|
|
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
|
|
<TrendingUp className="w-4.5 h-4.5 text-[#0F6E56]" />
|
|
<span>User Cohort growth curves</span>
|
|
</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Employer and Worker registry trends (6 Months)</p>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<svg className="w-full h-56" viewBox="0 0 450 200">
|
|
{/* Grids */}
|
|
<line x1="40" y1="20" x2="435" y2="20" stroke="#f8fafc" strokeWidth="1.5" />
|
|
<line x1="40" y1="75" x2="435" y2="75" stroke="#f8fafc" strokeWidth="1.5" />
|
|
<line x1="40" y1="130" x2="435" y2="130" stroke="#f8fafc" strokeWidth="1.5" />
|
|
<line x1="40" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1.5" />
|
|
|
|
{/* Worker Area fill */}
|
|
<polygon
|
|
fill="url(#workerGrad)"
|
|
points={getCoordinatesForArea('workers', 1500)}
|
|
className="opacity-45"
|
|
/>
|
|
{/* Worker Line */}
|
|
<polyline
|
|
fill="none"
|
|
stroke="#10b981"
|
|
strokeWidth="4"
|
|
points={getLineCoordinates('workers', 1500)}
|
|
/>
|
|
|
|
{/* Employer Line */}
|
|
<polyline
|
|
fill="none"
|
|
stroke="#3b82f6"
|
|
strokeWidth="3.5"
|
|
strokeDasharray="4 4"
|
|
points={getLineCoordinates('employers', 1500)}
|
|
/>
|
|
|
|
{/* Gradients */}
|
|
<defs>
|
|
<linearGradient id="workerGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#10b981" />
|
|
<stop offset="100%" stopColor="#ffffff" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
{/* X-Axis Month labels */}
|
|
{user_growth.map((d, i) => {
|
|
const x = 40 + (i / 5) * 380;
|
|
return (
|
|
<text key={i} x={x} y="195" textAnchor="middle" className="text-[9px] font-bold fill-slate-400 font-mono">
|
|
{d.month}
|
|
</text>
|
|
);
|
|
})}
|
|
|
|
{/* Y Labels */}
|
|
<text x="30" y="24" textAnchor="end" className="text-[9px] font-black fill-slate-300">1.5K</text>
|
|
<text x="30" y="80" textAnchor="end" className="text-[9px] font-black fill-slate-300">750</text>
|
|
<text x="30" y="135" textAnchor="end" className="text-[9px] font-black fill-slate-300">300</text>
|
|
<text x="30" y="185" textAnchor="end" className="text-[9px] font-black fill-slate-300">0</text>
|
|
</svg>
|
|
|
|
<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.5 h-3.5 bg-emerald-500 rounded" />
|
|
<span>Workers ({user_growth[5].workers})</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3.5 h-1 border-t-2 border-dashed border-blue-500" />
|
|
<span>Employers ({user_growth[5].employers})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Revenue reports */}
|
|
{selectedTab === 'Revenue' && (
|
|
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
|
|
<CreditCard className="w-4.5 h-4.5 text-[#0F6E56]" />
|
|
<span>Stacked Subscription revenue splits</span>
|
|
</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Monthly billing splits (AED)</p>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<svg className="w-full h-56" viewBox="0 0 450 200">
|
|
<line x1="40" y1="180" x2="435" y2="180" stroke="#cbd5e1" strokeWidth="1.5" />
|
|
|
|
{/* Stacked bars for each month */}
|
|
{revenue_breakdown.map((d, i) => {
|
|
const x = 50 + i * 62;
|
|
const total = d.basic + d.premium + d.vip;
|
|
const maxVal = 80000;
|
|
const scale = 150; // Graph height scale factor
|
|
|
|
const basicH = (d.basic / maxVal) * scale;
|
|
const premiumH = (d.premium / maxVal) * scale;
|
|
const vipH = (d.vip / maxVal) * scale;
|
|
|
|
const basicY = 180 - basicH;
|
|
const premiumY = basicY - premiumH;
|
|
const vipY = premiumY - vipH;
|
|
|
|
return (
|
|
<g key={i} className="cursor-pointer group">
|
|
{/* Basic */}
|
|
<rect x={x} y={basicY} width="22" height={basicH} fill="#94a3b8" rx="2" />
|
|
{/* Premium */}
|
|
<rect x={x} y={premiumY} width="22" height={premiumH} fill="#3b82f6" />
|
|
{/* VIP */}
|
|
<rect x={x} y={vipY} width="22" height={vipH} fill="#10b981" rx="2" />
|
|
|
|
<text x={x + 11} y="195" textAnchor="middle" className="text-[9px] font-bold fill-slate-400 font-mono">
|
|
{d.month.split(' ')[0]}
|
|
</text>
|
|
</g>
|
|
);
|
|
})}
|
|
|
|
<text x="30" y="30" textAnchor="end" className="text-[9px] font-black fill-slate-300">80K</text>
|
|
<text x="30" y="105" textAnchor="end" className="text-[9px] font-black fill-slate-300">40K</text>
|
|
<text x="30" y="180" textAnchor="end" className="text-[9px] font-black fill-slate-300">0</text>
|
|
</svg>
|
|
|
|
<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-3 bg-slate-400 rounded-sm" />
|
|
<span>Basic</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3 h-3 bg-blue-500 rounded-sm" />
|
|
<span>Premium</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 text-xs font-bold text-slate-600">
|
|
<div className="w-3 h-3 bg-emerald-500 rounded-sm" />
|
|
<span>VIP</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Funnel Conversions */}
|
|
{selectedTab === 'Funnel' && (
|
|
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm space-y-6">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
|
|
<Percent className="w-4.5 h-4.5 text-[#0F6E56]" />
|
|
<span>Hiring Placement Funnel ratios</span>
|
|
</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Hiring Conversion stage details (This month)</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{hiring_funnel.map((item, idx) => {
|
|
const maxVal = hiring_funnel[0].count;
|
|
const pct = ((item.count / maxVal) * 100).toFixed(1);
|
|
return (
|
|
<div key={idx} className="space-y-2 text-xs font-bold text-slate-700">
|
|
<div className="flex items-center justify-between">
|
|
<span>{item.stage}</span>
|
|
<span>{item.count.toLocaleString()} ({pct}%)</span>
|
|
</div>
|
|
<div className="w-full bg-slate-100 rounded-full h-3.5 overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full bg-gradient-to-r from-teal-600 to-teal-500 transition-all duration-500"
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Right side cohort Retention Analysis: Month-over-Month curves */}
|
|
<div className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm flex flex-col justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest flex items-center gap-1.5">
|
|
<Award className="w-4.5 h-4.5 text-[#0F6E56]" />
|
|
<span>Subscription Cohort Retention rates</span>
|
|
</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5 uppercase tracking-wider font-bold">Month 1 to Month 6 user retention metrics</p>
|
|
</div>
|
|
|
|
<div className="space-y-4 my-6">
|
|
{retention_rates.map((cohort, i) => (
|
|
<div key={i} className="flex items-center justify-between text-xs font-bold text-slate-700">
|
|
<span className="w-20">{cohort.cohort}</span>
|
|
<div className="flex-1 mx-4 bg-slate-100 rounded-full h-3 overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-blue-600 to-blue-500 rounded-full"
|
|
style={{ width: `${cohort.rate}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-blue-600 w-10 text-right">{cohort.rate}%</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="bg-emerald-50/50 p-4 border border-emerald-100 rounded-2xl text-[11px] font-bold text-teal-800 leading-relaxed shadow-sm">
|
|
<Sparkles className="w-4 h-4 text-emerald-600 mb-1" />
|
|
<span>System Insights: Employer VIP and Premium tier subscribers yield a 79% retention rate beyond Month 3, validating platform organic vetting value.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|