314 lines
12 KiB
TypeScript
314 lines
12 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import type { User, Asset } from "../../../types";
|
|
import { apiClient } from "../../../lib/api-client";
|
|
import {
|
|
Users,
|
|
FileStack,
|
|
DownloadCloud,
|
|
AlertTriangle,
|
|
Cpu,
|
|
Terminal,
|
|
Brain,
|
|
Server,
|
|
ShieldCheck,
|
|
} from "lucide-react";
|
|
|
|
export const AnalyticsDashboard: React.FC = () => {
|
|
const [clients, setClients] = useState<User[]>([]);
|
|
const [assets, setAssets] = useState<Asset[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fetchStats = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const clientRes = await apiClient.get<User[]>("/admin/clients");
|
|
const assetRes = await apiClient.get<Asset[]>("/assets");
|
|
setClients(clientRes.data);
|
|
setAssets(assetRes.data);
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchStats();
|
|
}, []);
|
|
|
|
// Compute analytics data
|
|
const totalClients = clients.length;
|
|
const totalAssets = assets.length;
|
|
const totalDownloads = assets.reduce((sum, a) => sum + a.downloadsCount, 0);
|
|
const pendingApprovals = clients.filter(
|
|
(c) => c.onboardingStatus === "PENDING_APPROVAL",
|
|
).length;
|
|
|
|
// Category counts
|
|
const categoryCounts = assets.reduce(
|
|
(acc, a) => {
|
|
acc[a.categoryId] = (acc[a.categoryId] || 0) + 1;
|
|
return acc;
|
|
},
|
|
{ silicon: 0, software: 0, ai: 0, cloud: 0 } as Record<string, number>,
|
|
);
|
|
|
|
const categoryMax = Math.max(...Object.values(categoryCounts), 1);
|
|
|
|
// Onboarding Status distributions
|
|
const onboardingCounts = clients.reduce(
|
|
(acc, c) => {
|
|
acc[c.onboardingStatus] = (acc[c.onboardingStatus] || 0) + 1;
|
|
return acc;
|
|
},
|
|
{
|
|
NOT_STARTED: 0,
|
|
FORM_COMPLETED: 0,
|
|
NDA_SIGNED: 0,
|
|
PENDING_APPROVAL: 0,
|
|
APPROVED: 0,
|
|
REJECTED: 0,
|
|
} as Record<string, number>,
|
|
);
|
|
|
|
const onboardingMax = Math.max(...Object.values(onboardingCounts), 1);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-ink-800">
|
|
Operational Analytics
|
|
</h2>
|
|
<p className="text-sm text-ink-600">
|
|
Overview of client onboarding metrics, asset repository distribution,
|
|
and core library downloads.
|
|
</p>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{[1, 2, 3, 4].map((n) => (
|
|
<div
|
|
key={n}
|
|
className="h-28 animate-pulse rounded-xl bg-white border border-ink-100"
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
/* Metric Cards */
|
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm flex items-center gap-4 hover:shadow-premium transition-all duration-300">
|
|
<div className="rounded-lg bg-primary-50 p-3 text-primary-700">
|
|
<Users className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-bold uppercase tracking-wider text-ink-600">
|
|
Total Clients
|
|
</p>
|
|
<p className="text-2xl font-bold text-ink-800">{totalClients}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm flex items-center gap-4 hover:shadow-premium transition-all duration-300">
|
|
<div className="rounded-lg bg-primary-50 p-3 text-primary-700">
|
|
<FileStack className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-bold uppercase tracking-wider text-ink-600">
|
|
Active Assets
|
|
</p>
|
|
<p className="text-2xl font-bold text-ink-800">{totalAssets}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm flex items-center gap-4 hover:shadow-premium transition-all duration-300">
|
|
<div className="rounded-lg bg-primary-50 p-3 text-primary-700">
|
|
<DownloadCloud className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-bold uppercase tracking-wider text-ink-600">
|
|
Total Downloads
|
|
</p>
|
|
<p className="text-2xl font-bold text-ink-800">
|
|
{totalDownloads}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm flex items-center gap-4 hover:shadow-premium transition-all duration-300">
|
|
<div
|
|
className={`rounded-lg p-3 ${pendingApprovals > 0 ? "bg-warning/10 text-warning" : "bg-success/10 text-success"}`}
|
|
>
|
|
<AlertTriangle className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] font-bold uppercase tracking-wider text-ink-600">
|
|
Pending Review
|
|
</p>
|
|
<p className="text-2xl font-bold text-ink-800">
|
|
{pendingApprovals}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Charts Grid */}
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
{/* Category Breakdown */}
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-bold text-ink-800">
|
|
Assets by Domain Category
|
|
</h3>
|
|
<span className="text-[10px] uppercase font-bold tracking-wider text-ink-600">
|
|
Count
|
|
</span>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="space-y-4 py-6">
|
|
{[1, 2, 3].map((n) => (
|
|
<div key={n} className="h-8 animate-pulse rounded bg-ink-50" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 py-2">
|
|
{[
|
|
{ key: "silicon", name: "Silicon Core IP", icon: Cpu },
|
|
{
|
|
key: "software",
|
|
name: "Software & Libraries",
|
|
icon: Terminal,
|
|
},
|
|
{ key: "ai", name: "AI & Agent Workflows", icon: Brain },
|
|
{ key: "cloud", name: "Cloud Configs & IaC", icon: Server },
|
|
].map((item) => {
|
|
const count = categoryCounts[item.key] || 0;
|
|
const percentage = (count / categoryMax) * 100;
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<div key={item.key} className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="flex items-center gap-1.5 font-semibold text-ink-700">
|
|
<Icon className="h-4 w-4 text-primary-700" />
|
|
{item.name}
|
|
</span>
|
|
<span className="font-mono font-bold text-ink-800">
|
|
{count}
|
|
</span>
|
|
</div>
|
|
<div className="h-3.5 w-full rounded-full bg-ink-50 overflow-hidden border border-ink-100/50">
|
|
<div
|
|
className="h-full rounded-full bg-gradient-to-r from-primary-200 to-primary-400 transition-all duration-500"
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Client Onboarding Funnel */}
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-bold text-ink-800">
|
|
Client Onboarding Funnel
|
|
</h3>
|
|
<span className="text-[10px] uppercase font-bold tracking-wider text-ink-600">
|
|
Accounts
|
|
</span>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="space-y-4 py-6">
|
|
{[1, 2, 3].map((n) => (
|
|
<div key={n} className="h-8 animate-pulse rounded bg-ink-50" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 py-2">
|
|
{[
|
|
{ key: "NOT_STARTED", name: "Registration Initiated" },
|
|
{ key: "FORM_COMPLETED", name: "Profile Form Saved" },
|
|
{ key: "NDA_SIGNED", name: "NDA Document Signed" },
|
|
{ key: "PENDING_APPROVAL", name: "Awaiting Admin Approval" },
|
|
{ key: "APPROVED", name: "Approved & Active Portal" },
|
|
].map((item) => {
|
|
const count = onboardingCounts[item.key] || 0;
|
|
const percentage = (count / onboardingMax) * 100;
|
|
|
|
return (
|
|
<div key={item.key} className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="font-semibold text-ink-700">
|
|
{item.name}
|
|
</span>
|
|
<span className="font-mono font-bold text-ink-800">
|
|
{count}
|
|
</span>
|
|
</div>
|
|
<div className="h-3.5 w-full rounded-full bg-ink-50 overflow-hidden border border-ink-100/50">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-500 ${
|
|
item.key === "APPROVED"
|
|
? "bg-success/70"
|
|
: item.key === "PENDING_APPROVAL"
|
|
? "bg-warning/70"
|
|
: "bg-primary-300"
|
|
}`}
|
|
style={{ width: `${percentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom list showing Top Downloaded Assets */}
|
|
<div className="rounded-xl border border-ink-100 bg-white p-5 shadow-sm space-y-4">
|
|
<h3 className="text-sm font-bold text-ink-800">
|
|
Most Downloaded Catalog Resources
|
|
</h3>
|
|
<div className="divide-y divide-ink-50">
|
|
{assets
|
|
.slice()
|
|
.sort((a, b) => b.downloadsCount - a.downloadsCount)
|
|
.slice(0, 3)
|
|
.map((asset, index) => (
|
|
<div
|
|
key={asset.id}
|
|
className="flex items-center justify-between py-2.5 first:pt-0 last:pb-0"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary-50 text-xs font-bold text-primary-800 border border-primary-100">
|
|
{index + 1}
|
|
</span>
|
|
<div>
|
|
<p className="text-xs font-bold text-ink-800">
|
|
{asset.title}
|
|
</p>
|
|
<p className="text-[10px] text-ink-600 font-semibold">
|
|
{asset.subcategory}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-xs text-ink-700 font-mono">
|
|
<ShieldCheck className="h-4 w-4 text-success" />
|
|
<span className="font-bold">{asset.downloadsCount}</span>{" "}
|
|
downloads
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|