Sunday, 9 November 2025

Hardware stock app

import React, { useEffect, useMemo, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Download, FileSpreadsheet, Import, Package, Plus, Search, Trash2, Edit, ArrowUpDown, UploadCloud, ArrowDownCircle, ArrowUpCircle, Factory, Filter } from "lucide-react"; import { v4 as uuidv4 } from "uuid"; // --- shadcn/ui components --- import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useLocalStorage } from "usehooks-ts"; // Helper types function currency(n) { if (n === undefined || n === null || isNaN(n)) return "-"; return new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" }).format(Number(n)); } const starterItems = [ { id: uuidv4(), name: "RJ45 Connector", sku: "NET-RJ45", category: "Networking", location: "Bin A1", qty: 120, minQty: 50, unit: "pcs", cost: 0.15, supplier: "NetParts Co." }, { id: uuidv4(), name: "SATA Cable 0.5m", sku: "ST-SATA05", category: "Cables", location: "Bin C3", qty: 35, minQty: 40, unit: "pcs", cost: 1.2, supplier: "CableX" }, { id: uuidv4(), name: "SSD 1TB", sku: "SSD-1TB", category: "Storage", location: "Shelf S2", qty: 14, minQty: 10, unit: "pcs", cost: 55.0, supplier: "FastDisk" }, { id: uuidv4(), name: "12V 2A Adapter", sku: "AD-12V2A", category: "Power", location: "Rack P1", qty: 6, minQty: 10, unit: "pcs", cost: 6.5, supplier: "PowerMax" }, ]; export default function HardwareStockManager() { const [items, setItems] = useLocalStorage("hardware-stock-items", starterItems); const [query, setQuery] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); const [lowOnly, setLowOnly] = useState(false); const [sort, setSort] = useState({ key: "name", dir: "asc" }); const [editing, setEditing] = useState(null); // item const [adjusting, setAdjusting] = useState(null); // {item, mode} const [toDelete, setToDelete] = useState(null); // item const [importOpen, setImportOpen] = useState(false); const [importText, setImportText] = useState(""); const categories = useMemo(() => { const set = new Set(items.map(i => i.category).filter(Boolean)); return ["Networking","Cables","Storage","Power", ...Array.from(set)].filter((v, idx, arr) => arr.indexOf(v) === idx); }, [items]); const filtered = useMemo(() => { let list = items.filter(i => { const q = query.trim().toLowerCase(); const inSearch = !q || [i.name, i.sku, i.category, i.location, i.supplier].some(f => String(f || "").toLowerCase().includes(q)); const inCat = categoryFilter === "all" || i.category === categoryFilter; const inLow = !lowOnly || (Number(i.qty) <= Number(i.minQty)); return inSearch && inCat && inLow; }); if (sort.key) { list = [...list].sort((a, b) => { const A = a[sort.key]; const B = b[sort.key]; if (A === B) return 0; if (sort.dir === "asc") return A > B ? 1 : -1; return A < B ? 1 : -1; }); } return list; }, [items, query, categoryFilter, lowOnly, sort]); const totals = useMemo(() => { const totalQty = items.reduce((s, i) => s + Number(i.qty || 0), 0); const totalValue = items.reduce((s, i) => s + Number(i.qty || 0) * Number(i.cost || 0), 0); const lowCount = items.filter(i => Number(i.qty) <= Number(i.minQty)).length; return { totalQty, totalValue, lowCount }; }, [items]); function upsertItem(item) { if (!item.id) item.id = uuidv4(); setItems(prev => { const idx = prev.findIndex(p => p.id === item.id); if (idx === -1) return [...prev, item]; const copy = [...prev]; copy[idx] = item; return copy; }); } function removeItem(id) { setItems(prev => prev.filter(i => i.id !== id)); } function exportCSV() { const headers = ["name","sku","category","location","qty","minQty","unit","cost","supplier","id"]; const rows = [headers.join(","), ...items.map(i => headers.map(h => formatCSV(String(i[h] ?? ""))).join(","))]; const blob = new Blob([rows.join("\n")], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `hardware-stock-${new Date().toISOString().slice(0,10)}.csv`; a.click(); URL.revokeObjectURL(url); } function formatCSV(value) { if (value.includes(",") || value.includes("\n") || value.includes('"')) { return '"' + value.replaceAll('"', '""') + '"'; } return value; } function parseCSV(text) { // lightweight CSV parser (no quotes-in-quotes edge cases beyond doubled quotes) const lines = text.trim().split(/\r?\n/); if (!lines.length) return []; const headers = lines[0].split(",").map(h => h.trim()); return lines.slice(1).map(line => { const cols = []; let cur = ""; let inQ = false; for (let i=0;i obj[h] = cols[idx]); // coerce numbers obj.qty = Number(obj.qty ?? 0); obj.minQty = Number(obj.minQty ?? 0); obj.cost = Number(obj.cost ?? 0); if (!obj.id) obj.id = uuidv4(); return obj; }); } function importCSV() { try { const rows = parseCSV(importText); if (!rows.length) return; setItems(prev => { const map = new Map(prev.map(p => [p.id, p])); rows.forEach(r => map.set(r.id, { ...map.get(r.id), ...r })); return Array.from(map.values()); }); setImportOpen(false); setImportText(""); } catch(e) { alert("Import failed. Please check CSV format."); } } function adjustQty(item, delta) { const next = { ...item, qty: Math.max(0, Number(item.qty || 0) + Number(delta)) }; upsertItem(next); } function headerCell(key, label) { return ( ); } return (

Hardware Stock Manager

Track, receive, issue, and audit your hardware inventory.

Import items from CSV Headers: name, sku, category, location, qty, minQty, unit, cost, supplier, id (optional).