feat(console,api): implement /console Admin Dashboard and Go backend full CRUD REST APIs
- Add comprehensive /console admin web UI for managing research projects, areas, members, publications, patents, lectures, and standards - Implement Admin token authentication middleware (RequireAdminToken) with Bearer token validation - Implement POST, PUT, DELETE REST API endpoints for all database entities across 5 domains - Add typed admin API client in refer_landing_page/lib/adminApi.ts with localStorage session persistence - Enhance E2E test runner to manage backend server lifecycle during automated tests - Pass all 12 quality gates cleanly with unanimous reviewer PASS verdicts
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AdminHeader, AlertBanner } from "./_components/AdminComponents";
|
||||
import {
|
||||
adminGetStatsSummary,
|
||||
adminGetResearchProjects,
|
||||
adminGetResearchAreas,
|
||||
adminGetMembers,
|
||||
adminGetAlumni,
|
||||
adminGetPublications,
|
||||
adminGetPatents,
|
||||
adminGetSemesters,
|
||||
adminGetStandardsBodies,
|
||||
AdminStatsSummary,
|
||||
} from "../../lib/adminApi";
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
const [stats, setStats] = useState<AdminStatsSummary | null>(null);
|
||||
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 [
|
||||
statsData,
|
||||
projects,
|
||||
areas,
|
||||
members,
|
||||
alumni,
|
||||
intlPubs,
|
||||
domPubs,
|
||||
patents,
|
||||
semesters,
|
||||
bodies,
|
||||
] = await Promise.all([
|
||||
adminGetStatsSummary().catch(() => null),
|
||||
adminGetResearchProjects().catch(() => []),
|
||||
adminGetResearchAreas().catch(() => []),
|
||||
adminGetMembers().catch(() => []),
|
||||
adminGetAlumni().catch(() => []),
|
||||
adminGetPublications("intl-journal-conf").catch(() => []),
|
||||
adminGetPublications("domestic-journal-conf").catch(() => []),
|
||||
adminGetPatents().catch(() => []),
|
||||
adminGetSemesters().catch(() => []),
|
||||
adminGetStandardsBodies().catch(() => []),
|
||||
]);
|
||||
|
||||
if (statsData) setStats(statsData);
|
||||
|
||||
const totalCourses = semesters.reduce((acc, s) => acc + (s.courses?.length || 0), 0);
|
||||
const totalDocs = bodies.reduce(
|
||||
(acc, b) => acc + (b.projects?.reduce((pAcc, p) => pAcc + (p.documents?.length || 0), 0) || 0),
|
||||
0
|
||||
);
|
||||
|
||||
setCounts({
|
||||
projects: projects.length,
|
||||
areas: areas.length,
|
||||
members: members.length,
|
||||
alumni: alumni.length,
|
||||
intlPubs: intlPubs.length,
|
||||
domPubs: domPubs.length,
|
||||
patents: patents.length,
|
||||
semesters: semesters.length,
|
||||
courses: totalCourses,
|
||||
bodies: bodies.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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user