From 29837dcdb86a7cd819962f4e4429e9c534ae2e64 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:38:36 +0000 Subject: [PATCH] implement reports page with asset overview and status breakdown --- frontend/app/(dashboard)/reports/page.tsx | 257 ++++++++++++++++++++++ frontend/package-lock.json | 13 -- 2 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 frontend/app/(dashboard)/reports/page.tsx diff --git a/frontend/app/(dashboard)/reports/page.tsx b/frontend/app/(dashboard)/reports/page.tsx new file mode 100644 index 00000000..108334d8 --- /dev/null +++ b/frontend/app/(dashboard)/reports/page.tsx @@ -0,0 +1,257 @@ +// frontend/app/(dashboard)/reports/page.tsx +"use client"; + +import Link from "next/link"; +import { format } from "date-fns"; +import { BarChart3, Package } from "lucide-react"; +import { clsx } from "clsx"; +import { useAssets } from "@/lib/query/hooks/useAsset"; +import { Asset, AssetStatus } from "@/lib/query/types/asset"; +import { StatusBadge } from "@/components/assets/status-badge"; + +const STATUS_COLORS: Record = { + [AssetStatus.ACTIVE]: "bg-green-500", + [AssetStatus.ASSIGNED]: "bg-blue-500", + [AssetStatus.MAINTENANCE]: "bg-yellow-500", + [AssetStatus.RETIRED]: "bg-gray-400", +}; + +export default function ReportsPage() { + const { data, isLoading } = useAssets({ page: 1, limit: 1000 }); + + if (isLoading) { + return ( +
+ Loading report… +
+ ); + } + + if (!data) return null; + + const assets = data?.assets ?? []; + const total = assets.length; + + const byStatus = assets.reduce>( + (acc, asset) => { + acc[asset.status] += 1; + return acc; + }, + { + [AssetStatus.ACTIVE]: 0, + [AssetStatus.ASSIGNED]: 0, + [AssetStatus.MAINTENANCE]: 0, + [AssetStatus.RETIRED]: 0, + }, + ); + + const byCategory = Object.values( + assets.reduce>((acc, asset) => { + const categoryName = asset.category?.name ?? "Uncategorized"; + if (!acc[categoryName]) { + acc[categoryName] = { name: categoryName, count: 0 }; + } + acc[categoryName].count += 1; + return acc; + }, {}), + ); + + const byDepartment = Object.values( + assets.reduce>((acc, asset) => { + const departmentName = asset.department?.name ?? "Unassigned"; + if (!acc[departmentName]) { + acc[departmentName] = { name: departmentName, count: 0 }; + } + acc[departmentName].count += 1; + return acc; + }, {}), + ); + + const recent = [...assets] + .sort( + (left, right) => + new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime(), + ) + .slice(0, 10); + + const statusItems = Object.entries(byStatus) as [AssetStatus, number][]; + const topCategories = [...byCategory] + .sort((a, b) => b.count - a.count) + .slice(0, 8); + const topDepartments = [...byDepartment] + .sort((a, b) => b.count - a.count) + .slice(0, 8); + + return ( +
+ {/* Header */} +
+

Reports

+

Asset inventory overview

+
+ + {/* Summary cards */} +
+
+

Total Assets

+

{total}

+
+ {statusItems.map(([status, count]) => ( +
+

+ {status.toLowerCase()} +

+

{count}

+
+ ))} +
+ +
+ {/* By Status bar */} +
+

+ + Assets by Status +

+
+ {statusItems.map(([status, count]) => { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {status.toLowerCase()} + + {count} ({pct}%) + +
+
+
+
+
+ ); + })} +
+
+ + {/* By Category */} +
+

+ + Assets by Category +

+ {topCategories.length === 0 ? ( +

No data

+ ) : ( +
+ {topCategories.map(({ name, count }) => { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {name} + + {count} ({pct}%) + +
+
+
+
+
+ ); + })} +
+ )} +
+
+ +
+ {/* By Department */} +
+

+ + Assets by Department +

+ {topDepartments.length === 0 ? ( +

No data

+ ) : ( +
+ {topDepartments.map(({ name, count }) => { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {name} + + {count} ({pct}%) + +
+
+
+
+
+ ); + })} +
+ )} +
+ + {/* Recent Assets */} +
+
+

+ + Recently Added +

+ + View all + +
+ {recent.length === 0 ? ( +

+ No assets yet +

+ ) : ( +
+ {recent.map((asset: Asset) => ( + +
+

+ {asset.name} +

+

+ {asset.assetId} · {asset.category?.name ?? "—"} ·{" "} + {format(new Date(asset.createdAt), "MMM d, yyyy")} +

+
+ + + ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b2dabd6..619fff03 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,7 +18,6 @@ "@tanstack/react-table": "^8.21.3", "axios": "^1.13.2", "clsx": "^2.1.1", - "create-next-app": "^15.5.9", "date-fns": "^4.1.0", "jspdf": "^4.0.0", "lucide-react": "^0.562.0", @@ -4497,18 +4496,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/create-next-app": { - "version": "15.5.9", - "resolved": "https://registry.npmjs.org/create-next-app/-/create-next-app-15.5.9.tgz", - "integrity": "sha512-B489ZPUb555IuCM4Ggevi3dzHqpzdZXUkEAT/RFz8JKG3HtxYC8PacOIjSKKgXDfbsHza+ckmoScjwRgaV4L7w==", - "license": "MIT", - "bin": { - "create-next-app": "dist/index.js" - }, - "engines": { - "node": ">=18.18.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",