From 1867846a9fd37f2cfbc33c5df9b5200dba585003 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 10 Apr 2026 06:21:43 +0000 Subject: [PATCH] feat(03-03): API client, TanStack Query hooks, layout shell, inventory item components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web/src/lib/api.ts: typed fetch wrappers for GET /api/inventory and GET /api/inventory/:id - web/src/hooks/useInventory.ts: useInventory + useInventoryItem TanStack Query hooks - web/src/components/inventory/StatusBadge.tsx: catalog_status → Badge variant mapping - web/src/components/inventory/ItemCard.tsx: grid card with photo, HW ID, name, status, NetBox link - web/src/components/inventory/ItemRow.tsx: list-mode row with status color indicator - web/src/components/layout/TopBar.tsx: sticky nav with HWLab volt brand, Add Item + Scan buttons - web/src/components/layout/AppShell.tsx: TopBar + main content wrapper --- web/src/components/inventory/ItemCard.tsx | 69 ++++++++++++++++++++ web/src/components/inventory/ItemRow.tsx | 58 ++++++++++++++++ web/src/components/inventory/StatusBadge.tsx | 16 +++++ web/src/components/layout/AppShell.tsx | 13 ++++ web/src/components/layout/TopBar.tsx | 27 ++++++++ web/src/hooks/useInventory.ts | 17 +++++ web/src/lib/api.ts | 30 +++++++++ 7 files changed, 230 insertions(+) create mode 100644 web/src/components/inventory/ItemCard.tsx create mode 100644 web/src/components/inventory/ItemRow.tsx create mode 100644 web/src/components/inventory/StatusBadge.tsx create mode 100644 web/src/components/layout/AppShell.tsx create mode 100644 web/src/components/layout/TopBar.tsx create mode 100644 web/src/hooks/useInventory.ts create mode 100644 web/src/lib/api.ts diff --git a/web/src/components/inventory/ItemCard.tsx b/web/src/components/inventory/ItemCard.tsx new file mode 100644 index 0000000..e5eb434 --- /dev/null +++ b/web/src/components/inventory/ItemCard.tsx @@ -0,0 +1,69 @@ +import { ExternalLink, Package } from 'lucide-react' +import { Link } from '@tanstack/react-router' +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { StatusBadge } from './StatusBadge' +import type { InventoryItem } from '@/lib/api' +import { cn } from '@/lib/utils' + +interface ItemCardProps { + item: InventoryItem + className?: string +} + +export function ItemCard({ item, className }: ItemCardProps) { + const netboxUrl = `http://netbox.local/dcim/devices/${item.id}/` + + return ( + + + {/* Photo */} +
+ {item.photo_urls.length > 0 ? ( + {item.name} + ) : ( + + )} +
+ + + {/* HW ID */} +

{item.hw_id || item.asset_tag}

+ {item.name} +
+ + + + {item.ai_notes && ( +

{item.ai_notes}

+ )} +
+ + + + +
+ + ) +} diff --git a/web/src/components/inventory/ItemRow.tsx b/web/src/components/inventory/ItemRow.tsx new file mode 100644 index 0000000..e19cf62 --- /dev/null +++ b/web/src/components/inventory/ItemRow.tsx @@ -0,0 +1,58 @@ +import { ExternalLink } from 'lucide-react' +import { Link } from '@tanstack/react-router' +import { Button } from '@/components/ui/button' +import { StatusBadge } from './StatusBadge' +import type { InventoryItem } from '@/lib/api' + +const STATUS_COLOR: Record = { + indexed: 'bg-green-500', + complete: 'bg-volt', + researched: 'bg-blue-500', + needs_research: 'bg-yellow-500', + draft: 'bg-charcoal', +} + +export function ItemRow({ item }: { item: InventoryItem }) { + const netboxUrl = `http://netbox.local/dcim/devices/${item.id}/` + const dot = STATUS_COLOR[item.catalog_status] ?? 'bg-charcoal' + + return ( + + {/* Status indicator */} +
+ + {/* HW ID */} + {item.hw_id || item.asset_tag} + + {/* Name */} + {item.name} + + {/* Status */} +
+ +
+ + {/* Notes preview */} + {item.ai_notes && ( + {item.ai_notes} + )} + + {/* Quick action */} + + + ) +} diff --git a/web/src/components/inventory/StatusBadge.tsx b/web/src/components/inventory/StatusBadge.tsx new file mode 100644 index 0000000..831e034 --- /dev/null +++ b/web/src/components/inventory/StatusBadge.tsx @@ -0,0 +1,16 @@ +import { Badge } from '@/components/ui/badge' + +const STATUS_LABELS: Record = { + draft: 'Draft', + indexed: 'Indexed', + needs_research: 'Needs Research', + researched: 'Researched', + complete: 'Complete', +} + +type BadgeVariant = 'default' | 'indexed' | 'draft' | 'needs_research' | 'researched' | 'complete' | 'destructive' + +export function StatusBadge({ status }: { status: string }) { + const variant = (status in STATUS_LABELS ? status : 'draft') as BadgeVariant + return {STATUS_LABELS[status] ?? status} +} diff --git a/web/src/components/layout/AppShell.tsx b/web/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..75da8ae --- /dev/null +++ b/web/src/components/layout/AppShell.tsx @@ -0,0 +1,13 @@ +import * as React from 'react' +import { TopBar } from './TopBar' + +export function AppShell({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+
+ ) +} diff --git a/web/src/components/layout/TopBar.tsx b/web/src/components/layout/TopBar.tsx new file mode 100644 index 0000000..757906a --- /dev/null +++ b/web/src/components/layout/TopBar.tsx @@ -0,0 +1,27 @@ +import { Plus, QrCode } from 'lucide-react' +import { Link } from '@tanstack/react-router' +import { Button } from '@/components/ui/button' + +export function TopBar() { + return ( +
+ + HWLab + +
+ + +
+
+ ) +} diff --git a/web/src/hooks/useInventory.ts b/web/src/hooks/useInventory.ts new file mode 100644 index 0000000..3105094 --- /dev/null +++ b/web/src/hooks/useInventory.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { fetchInventory, fetchInventoryItem } from '@/lib/api' + +export function useInventory() { + return useQuery({ + queryKey: ['inventory'], + queryFn: fetchInventory, + }) +} + +export function useInventoryItem(id: number) { + return useQuery({ + queryKey: ['inventory', id], + queryFn: () => fetchInventoryItem(id), + enabled: id > 0, + }) +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..904a3b2 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,30 @@ +export interface InventoryItem { + id: number + name: string + asset_tag: string + hw_id: string + catalog_status: string + product_url?: string + firmware_version?: string + test_date?: string + test_data?: string + ai_notes?: string + photo_urls: string[] +} + +const BASE = '/api' + +async function fetchJSON(url: string): Promise { + const res = await fetch(url) + if (!res.ok) { + const body = await res.json().catch(() => ({ error: res.statusText })) + throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`) + } + return res.json() as Promise +} + +export const fetchInventory = (): Promise => + fetchJSON(`${BASE}/inventory`) + +export const fetchInventoryItem = (id: number): Promise => + fetchJSON(`${BASE}/inventory/${id}`)