From 3ebd7efd7fce3d9e888c1771041f4962e9d64e78 Mon Sep 17 00:00:00 2001 From: toanhoang Date: Mon, 24 Aug 2026 20:28:30 -0400 Subject: [PATCH] Assets page: show model name instead of UUID, row index instead of raw ID, add filters Model ID column now shows "Manufacturer Name" (looked up from the already-fetched asset-models list) instead of the raw model_id UUID. The ID column shows a 1-based row index within the current filtered view instead of the AssetItem UUID -- the id itself was never actionable in the UI (no per-row link/detail view exists), just clutter at this scale (~600+ rows after the real-data import). Added a filter bar: free-text search (matches serial_number or asset_tag, case-insensitive substring), status dropdown, model dropdown -- all client-side (assetItems is already the full list via useApiList, and filtering ~600 rows in memory is trivial, no need for a server-side filtered endpoint at this scale). tsc/lint clean, no Vitest coverage -- same deviation as the rest of this branch, see CLAUDE.md. --- src/app/assets/page.tsx | 93 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/src/app/assets/page.tsx b/src/app/assets/page.tsx index 7218e08..19fd7bb 100644 --- a/src/app/assets/page.tsx +++ b/src/app/assets/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, type FormEvent } from "react"; +import { useMemo, useState, type FormEvent } from "react"; import { useApiList } from "@/hooks/use-api-list"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -85,6 +85,37 @@ export default function AssetsPage() { const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + // Filter/search state -- client-side only (assetItems is already the + // full list from useApiList, and this app's actual scale, ~600 rows + // after the 2026-08-24 real-data import, is well within "just filter + // in memory" territory; no need for a server-side filtered endpoint). + const [searchText, setSearchText] = useState(""); + const [filterStatus, setFilterStatus] = useState("all"); + const [filterModelId, setFilterModelId] = useState("all"); + + // modelId -> "Manufacturer Name" -- avoids showing the raw model_id UUID + // in the table (2026-08-25 -- was previously the literal FK value). + const modelNameById = useMemo(() => { + const map = new Map(); + for (const m of assetModels) { + map.set(m.id, `${m.manufacturer} ${m.name}`); + } + return map; + }, [assetModels]); + + const filteredAssetItems = useMemo(() => { + const query = searchText.trim().toLowerCase(); + return assetItems.filter((item) => { + if (filterStatus !== "all" && item.status !== filterStatus) return false; + if (filterModelId !== "all" && item.model_id !== filterModelId) return false; + if (query) { + const haystack = `${item.serial_number} ${item.asset_tag ?? ""}`.toLowerCase(); + if (!haystack.includes(query)) return false; + } + return true; + }); + }, [assetItems, searchText, filterStatus, filterModelId]); + const [importStatus, setImportStatus] = useState<"idle" | "uploading" | "done" | "error">( "idle" ); @@ -208,6 +239,56 @@ export default function AssetsPage() {

Assets

+
+
+ + setSearchText(event.target.value)} + placeholder="e.g. DANLAP001" + className="w-64" + /> +
+ +
+ + +
+ +
+ + +
+ +

+ {filteredAssetItems.length} / {assetItems.length} assets +

+
+ {loading ? (

Loading...

) : error ? ( @@ -216,10 +297,10 @@ export default function AssetsPage() { - ID + # Serial Number Asset Tag - Model ID + Model Purchase Date Warranty End Date Status @@ -230,12 +311,12 @@ export default function AssetsPage() { - {assetItems.map((assetItem) => ( + {filteredAssetItems.map((assetItem, index) => ( - {assetItem.id} + {index + 1} {assetItem.serial_number} {assetItem.asset_tag ?? "-"} - {assetItem.model_id} + {modelNameById.get(assetItem.model_id) ?? assetItem.model_id} {assetItem.purchase_date ?? "-"} {assetItem.warranty_end_date ?? "-"} {assetItem.status}