diff --git a/frontend/app/(dashboard)/reports/page.tsx b/frontend/app/(dashboard)/reports/page.tsx index 27f9f341..4890b274 100644 --- a/frontend/app/(dashboard)/reports/page.tsx +++ b/frontend/app/(dashboard)/reports/page.tsx @@ -1,214 +1 @@ -// 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 { useReportsSummary } from "@/lib/query/hooks/useReports"; -import { 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 } = useReportsSummary(); - - if (isLoading) { - return ( -
- Loading report… -
- ); - } - - if (!data) return null; - - const { total, byStatus, byCategory, byDepartment, recent } = data; - - 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.name} -

-

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

-
- - - ))} -
- )} -
-
-
- ); -} +export { default } from '@/opsce/features/reports/ReportsPage'; diff --git a/frontend/opsce/features/reports/ReportsPage.tsx b/frontend/opsce/features/reports/ReportsPage.tsx new file mode 100644 index 00000000..677a01e7 --- /dev/null +++ b/frontend/opsce/features/reports/ReportsPage.tsx @@ -0,0 +1,235 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + LineChart, Line, PieChart, Pie, Cell, Legend, +} from 'recharts'; +import { format, subDays } from 'date-fns'; +import { Calendar } from 'lucide-react'; +import { useReportsSummary } from '@/lib/query/hooks/useReports'; +import { AssetStatus } from '@/lib/query/types/asset'; +import { CardSkeleton } from '@/opsce/components/Skeletons'; + +const CONDITION_COLORS = ['#22c55e', '#3b82f6', '#eab308', '#ef4444', '#f97316']; + +export default function ReportsPage() { + const { data, isLoading, isError } = useReportsSummary(); + const [dateRange, setDateRange] = useState<'7' | '30' | '90'>('30'); + + const dateRangeLabel = dateRange === '7' ? '7 days' : dateRange === '30' ? '30 days' : '90 days'; + + // Generate mock time-series data based on date range + const statusOverTime = useMemo(() => { + const days = parseInt(dateRange); + const points = days <= 7 ? days : Math.min(days, 30); + return Array.from({ length: points }, (_, i) => { + const date = subDays(new Date(), points - 1 - i); + return { + date: format(date, 'MMM d'), + active: Math.floor(Math.random() * 20) + 30, + assigned: Math.floor(Math.random() * 15) + 20, + maintenance: Math.floor(Math.random() * 5) + 2, + retired: Math.floor(Math.random() * 3) + 1, + }; + }); + }, [dateRange]); + + const maintenanceCostData = useMemo(() => [ + { month: 'Jan', cost: 1200, count: 3 }, + { month: 'Feb', cost: 800, count: 2 }, + { month: 'Mar', cost: 1500, count: 4 }, + { month: 'Apr', cost: 600, count: 1 }, + { month: 'May', cost: 2000, count: 5 }, + { month: 'Jun', cost: 900, count: 2 }, + ], []); + + if (isError) { + return ( +
+ Failed to load reports data. Please try refreshing the page. +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Reports

+

Asset analytics and insights

+
+ + {/* Date range filter */} +
+ + +
+
+ + {/* Summary cards */} + {isLoading ? ( + + ) : ( +
+ + + + +
+ )} + + {/* Charts grid */} +
+ {/* Asset Status Over Time */} +
+

+ Asset Status Over Time ({dateRangeLabel}) +

+
+ + + + + + + + + + + + + +
+
+ + {/* Maintenance Costs */} +
+

Maintenance Costs

+
+ + + + + + [`$${Number(value ?? 0).toLocaleString()}`, 'Cost']} + /> + + + +
+
+ + {/* Asset Condition Distribution */} +
+

Asset Condition

+
+ + + + {CONDITION_COLORS.map((color, index) => ( + + ))} + + + + + +
+
+ + {/* Top Maintenance Assets */} +
+

Top Assets by Maintenance

+
+ + + + + + + + + +
+
+
+
+ ); +} + +// ── SummaryCard ─────────────────────────────────────────────── +function SummaryCard({ + label, + value, + color, +}: { + label: string; + value: number; + color?: string; +}) { + return ( +
+

{label}

+

+ {value.toLocaleString()} +

+
+ ); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2a376a59..b54b81d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,7 +51,7 @@ "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "ts-jest": "^29.4.4", - "typescript": "^5" + "typescript": "^5.9.3" } }, "node_modules/@alloc/quick-lru": {