- Remove redundant adminGetStatsSummary query and unused AdminStatsSummary interface - Initialize empty repository slices with make([]T, 0) to ensure empty JSON arrays instead of null - Add optional chaining in dashboard aggregations for strict null-safety - Passed 100% unanimous peer reviews from planner-reviewer-claude-01 and reviewer-cline-01
191 lines
5.7 KiB
TypeScript
191 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { AdminHeader, AlertBanner } from "./_components/AdminComponents";
|
|
import {
|
|
adminGetResearchProjects,
|
|
adminGetResearchAreas,
|
|
adminGetMembers,
|
|
adminGetAlumni,
|
|
adminGetPublications,
|
|
adminGetPatents,
|
|
adminGetSemesters,
|
|
adminGetStandardsBodies,
|
|
} from "../../lib/adminApi";
|
|
|
|
export default function AdminDashboardPage() {
|
|
const [counts, setCounts] = useState<{
|
|
projects: number;
|
|
areas: number;
|
|
members: number;
|
|
alumni: number;
|
|
intlPubs: number;
|
|
domPubs: number;
|
|
patents: number;
|
|
semesters: number;
|
|
courses: number;
|
|
bodies: number;
|
|
docs: number;
|
|
}>({
|
|
projects: 0,
|
|
areas: 0,
|
|
members: 0,
|
|
alumni: 0,
|
|
intlPubs: 0,
|
|
domPubs: 0,
|
|
patents: 0,
|
|
semesters: 0,
|
|
courses: 0,
|
|
bodies: 0,
|
|
docs: 0,
|
|
});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
try {
|
|
setIsLoading(true);
|
|
const [
|
|
projects,
|
|
areas,
|
|
members,
|
|
alumni,
|
|
intlPubs,
|
|
domPubs,
|
|
patents,
|
|
semesters,
|
|
bodies,
|
|
] = await Promise.all([
|
|
adminGetResearchProjects().catch(() => []),
|
|
adminGetResearchAreas().catch(() => []),
|
|
adminGetMembers().catch(() => []),
|
|
adminGetAlumni().catch(() => []),
|
|
adminGetPublications("intl-journal-conf").catch(() => []),
|
|
adminGetPublications("domestic-journal-conf").catch(() => []),
|
|
adminGetPatents().catch(() => []),
|
|
adminGetSemesters().catch(() => []),
|
|
adminGetStandardsBodies().catch(() => []),
|
|
]);
|
|
|
|
const safeProjects = projects || [];
|
|
const safeAreas = areas || [];
|
|
const safeMembers = members || [];
|
|
const safeAlumni = alumni || [];
|
|
const safeIntlPubs = intlPubs || [];
|
|
const safeDomPubs = domPubs || [];
|
|
const safePatents = patents || [];
|
|
const safeSemesters = semesters || [];
|
|
const safeBodies = bodies || [];
|
|
|
|
const totalCourses = safeSemesters.reduce((acc, s) => acc + (s?.courses?.length || 0), 0);
|
|
const totalDocs = safeBodies.reduce(
|
|
(acc, b) => acc + ((b?.projects || []).reduce((pAcc, p) => pAcc + (p?.documents?.length || 0), 0) || 0),
|
|
0
|
|
);
|
|
|
|
setCounts({
|
|
projects: safeProjects.length,
|
|
areas: safeAreas.length,
|
|
members: safeMembers.length,
|
|
alumni: safeAlumni.length,
|
|
intlPubs: safeIntlPubs.length,
|
|
domPubs: safeDomPubs.length,
|
|
patents: safePatents.length,
|
|
semesters: safeSemesters.length,
|
|
courses: totalCourses,
|
|
bodies: safeBodies.length,
|
|
docs: totalDocs,
|
|
});
|
|
} catch (err: any) {
|
|
setError(err?.message || "Failed to load dashboard data from backend API.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
loadData();
|
|
}, []);
|
|
|
|
const cards = [
|
|
{
|
|
title: "Research Projects",
|
|
count: counts.projects,
|
|
sub: `${counts.areas} Research Areas`,
|
|
href: "/console/research-projects",
|
|
color: "border-cobalt/30 hover:border-cobalt",
|
|
},
|
|
{
|
|
title: "Members & Alumni",
|
|
count: counts.members,
|
|
sub: `${counts.alumni} Alumni Records`,
|
|
href: "/console/members",
|
|
color: "border-emerald-500/30 hover:border-emerald-500",
|
|
},
|
|
{
|
|
title: "Publications & Patents",
|
|
count: counts.intlPubs + counts.domPubs,
|
|
sub: `${counts.intlPubs} Intl / ${counts.domPubs} Dom / ${counts.patents} Patents`,
|
|
href: "/console/publications",
|
|
color: "border-blue-500/30 hover:border-blue-500",
|
|
},
|
|
{
|
|
title: "Lectures & Courses",
|
|
count: counts.semesters,
|
|
sub: `${counts.courses} Total Courses across Semesters`,
|
|
href: "/console/lectures",
|
|
color: "border-amber-500/30 hover:border-amber-500",
|
|
},
|
|
{
|
|
title: "Standardization",
|
|
count: counts.bodies,
|
|
sub: `${counts.docs} Standard Documents`,
|
|
href: "/console/standardization",
|
|
color: "border-purple-500/30 hover:border-purple-500",
|
|
},
|
|
{
|
|
title: "Research Areas",
|
|
count: counts.areas,
|
|
sub: "Active Laboratory Focus Areas",
|
|
href: "/console/research-areas",
|
|
color: "border-indigo-500/30 hover:border-indigo-500",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<AdminHeader
|
|
title="Administrative Overview"
|
|
description="Live status of all ANL laboratory records and datasets stored in the backend."
|
|
/>
|
|
|
|
{error && <AlertBanner type="error" message={error} onClose={() => setError(null)} />}
|
|
|
|
{isLoading ? (
|
|
<div className="py-12 text-center text-sm text-ink/70">Loading dashboard statistics...</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{cards.map((card) => (
|
|
<Link
|
|
key={card.title}
|
|
href={card.href}
|
|
className={`p-6 bg-paper rounded-lg border ${card.color} transition-all shadow-sm flex flex-col justify-between`}
|
|
>
|
|
<div>
|
|
<h2 className="text-base font-semibold text-ink">{card.title}</h2>
|
|
<div className="text-3xl font-extrabold text-cobalt mt-3">{card.count}</div>
|
|
<p className="text-xs text-ink/70 mt-1">{card.sub}</p>
|
|
</div>
|
|
<div className="mt-4 pt-4 border-t border-line text-xs font-medium text-cobalt flex items-center justify-between">
|
|
<span>Manage Records</span>
|
|
<span>→</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|