import { useState, useRef, useEffect } from "react"; import { chemicalsApi } from "../lib/api"; import type { ChemicalInventory } from "../shared/types"; import { Card } from "./ui/card"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { Badge } from "./ui/badge"; import { Camera, Sparkles, Mail, Search, Plus, AlertTriangle, ChevronDown, ChevronUp, Loader2, Trash2, FlaskConical, } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "./ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select"; // ── constants ────────────────────────────────────────────────────────────── const storageDeviceOptions = [ "Aerosol Can","Ampule","Bulked Item","Fiber Box","Gas Cylinder", "Glass Bottle","Metal Can","Metal Drum","Metal Open Drum","Pallet", "Plastic Bag","Plastic Bottle","Plastic Drum","Plastic Open Drum", ]; const physicalStates = ["Solid", "Liquid", "Gas"]; const unitOptions = ["mL", "L", "g", "kg", "mg", "oz", "lb", "gal", "mol"]; // ── helpers ──────────────────────────────────────────────────────────────── function daysUntil(dateStr: string) { const today = new Date(); today.setHours(0, 0, 0, 0); return Math.ceil((new Date(dateStr).getTime() - today.getTime()) / 86400000); } function fillBarColor(pct: number) { if (pct < 20) return "bg-red-500"; if (pct < 40) return "bg-amber-500"; return "bg-[#5a9584]"; } type UserProfile = { pi_first_name: string; bldg_code: string; lab: string; contact?: string }; function emptyForm(profile?: UserProfile | null): Partial { return { piFirstName: profile?.pi_first_name ?? "", bldgCode: profile?.bldg_code ?? "", lab: profile?.lab ?? "", contact: profile?.contact ?? "", physicalState: "", chemicalName: "", storageLocation: "", storageDevice: "Glass Bottle", numberOfContainers: "1", amountPerContainer: "", unitOfMeasure: "", casNumber: "", chemicalFormula: "", molecularWeight: "", vendor: "", catalogNumber: "", lotNumber: "", expirationDate: "", concentration: "", percentageFull: undefined, comments: "", }; } // ── field helper ─────────────────────────────────────────────────────────── function Field({ label, value, required }: { label: string; value?: string | number; required?: boolean }) { if (!value && value !== 0) return null; return (

{label}

{value}

); } // ── main component ───────────────────────────────────────────────────────── export function Inventory() { const [inventory, setInventory] = useState([]); const [profile, setProfile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [expandedId, setExpandedId] = useState(null); // add/edit dialog const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [form, setForm] = useState>(emptyForm()); const [isSaving, setIsSaving] = useState(false); const [formError, setFormError] = useState(""); const [showOptional, setShowOptional] = useState(false); // scan dialog const [scanOpen, setScanOpen] = useState(false); const [capturedImage, setCapturedImage] = useState(null); const [isProcessing, setIsProcessing] = useState(false); const [extractedData, setExtractedData] = useState | null>(null); const fileInputRef = useRef(null); useEffect(() => { Promise.all([ chemicalsApi.list(), fetch("/api/profile", { credentials: "include" }).then(r => r.ok ? r.json() : null), ]).then(([chems, prof]) => { setInventory(chems); setProfile(prof); }).finally(() => setIsLoading(false)); }, []); // ── form helpers ──────────────────────────────────────────────────────── function openAdd() { setEditingItem(null); setForm(emptyForm(profile)); setFormError(""); setShowOptional(false); setDialogOpen(true); } function openEdit(item: ChemicalInventory) { setEditingItem(item); setForm({ ...item }); setFormError(""); setShowOptional(true); setDialogOpen(true); } function setField(key: keyof ChemicalInventory, value: string | number | undefined) { setForm(f => ({ ...f, [key]: value })); } async function handleSave() { const required: (keyof ChemicalInventory)[] = [ "piFirstName","physicalState","chemicalName","bldgCode","lab", "storageLocation","storageDevice","numberOfContainers","amountPerContainer", "unitOfMeasure","casNumber", ]; const missing = required.filter(k => !form[k]); if (missing.length) { setFormError("Please fill in all required fields."); return; } setFormError(""); setIsSaving(true); try { if (editingItem) { const updated = await chemicalsApi.update(editingItem.id, form); setInventory(inv => inv.map(c => c.id === editingItem.id ? updated : c)); } else { const saved = await chemicalsApi.create(form as ChemicalInventory); setInventory(inv => [saved, ...inv]); } setDialogOpen(false); } catch { setFormError("Failed to save. Please try again."); } finally { setIsSaving(false); } } async function handleDelete(id: string) { if (!confirm("Remove this chemical from inventory?")) return; await chemicalsApi.remove(id); setInventory(inv => inv.filter(c => c.id !== id)); if (expandedId === id) setExpandedId(null); } // ── scan helpers ──────────────────────────────────────────────────────── function handlePhotoCapture(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { setCapturedImage(reader.result as string); setIsProcessing(true); setTimeout(() => { setExtractedData({ chemicalName: "Methanol", casNumber: "67-56-1", physicalState: "Liquid", vendor: "Sigma-Aldrich", lotNumber: "SLCD7890", catalogNumber: "34860", amountPerContainer: "1", unitOfMeasure: "L", numberOfContainers: "1", storageDevice: "Glass Bottle", chemicalFormula: "CH3OH", molecularWeight: "32.04", expirationDate: "2026-03-15", concentration: "99.8%", percentageFull: 95, needsManualEntry: ["piFirstName", "bldgCode", "lab", "storageLocation"], scannedImage: reader.result as string, piFirstName: profile?.pi_first_name ?? "", bldgCode: profile?.bldg_code ?? "", lab: profile?.lab ?? "", }); setIsProcessing(false); }, 2000); }; reader.readAsDataURL(file); } async function handleAddFromScan() { if (!extractedData) return; try { const saved = await chemicalsApi.create({ ...extractedData, piFirstName: extractedData.piFirstName || "", physicalState: extractedData.physicalState || "", chemicalName: extractedData.chemicalName || "", bldgCode: extractedData.bldgCode || "", lab: extractedData.lab || "", storageLocation: extractedData.storageLocation || "", storageDevice: extractedData.storageDevice || "Glass Bottle", numberOfContainers: extractedData.numberOfContainers || "1", amountPerContainer: extractedData.amountPerContainer || "", unitOfMeasure: extractedData.unitOfMeasure || "", casNumber: extractedData.casNumber || "", }); setInventory(inv => [saved, ...inv]); setCapturedImage(null); setExtractedData(null); setScanOpen(false); } catch (err) { console.error(err); } } // ── derived ───────────────────────────────────────────────────────────── const filtered = inventory.filter(c => c.chemicalName.toLowerCase().includes(searchQuery.toLowerCase()) || c.casNumber.includes(searchQuery) || c.lab.toLowerCase().includes(searchQuery.toLowerCase()) || (c.storageLocation || "").toLowerCase().includes(searchQuery.toLowerCase()) ); const lowStockCount = inventory.filter(c => c.percentageFull != null && c.percentageFull < 20).length; // ── render ─────────────────────────────────────────────────────────────── return (
{/* Header */}

Chemical Inventory

Track, manage, and monitor your lab chemicals

{/* Add manually */} {/* Scan */} Scan Chemical Label
{!capturedImage ? (
fileInputRef.current?.click()} >

Click to capture or upload a label photo

AI will extract all available information

) : (
Scanned label {isProcessing ? (

Scanning label…

Reading all visible information with AI

) : extractedData ? (

Information extracted. Yellow fields need manual entry.

{(["piFirstName","bldgCode","lab","storageLocation"] as const).map(k => (
)[k] || ""} onChange={e => setExtractedData({ ...extractedData, [k]: e.target.value })} />
))}
setExtractedData({...extractedData, chemicalName: e.target.value})} />
setExtractedData({...extractedData, casNumber: e.target.value})} />
setExtractedData({...extractedData, amountPerContainer: e.target.value})} />
setExtractedData({...extractedData, unitOfMeasure: e.target.value})} />
setExtractedData({...extractedData, expirationDate: e.target.value})} />
setExtractedData({...extractedData, percentageFull: parseFloat(e.target.value)})} />
) : null}
)}
{/* Email receipt banner */}

Auto-import receipts: email them to{" "} inventory@labwise-auto.com {" "}and we'll extract and add chemicals automatically.

{/* Search + stats */}
setSearchQuery(e.target.value)} />
{inventory.length} chemicals {lowStockCount > 0 && ( <> · {lowStockCount} low stock )}
{/* List */} {isLoading ? (
) : filtered.length === 0 ? (
{inventory.length === 0 ? "No chemicals yet. Add one to get started." : "No chemicals match your search."}
) : (
{filtered.map(item => { const isExpanded = expandedId === item.id; const days = item.expirationDate ? daysUntil(item.expirationDate) : null; const expired = days !== null && days < 0; const expiringSoon = days !== null && days >= 0 && days <= 30; const lowStock = item.percentageFull != null && item.percentageFull < 20; return ( {/* Summary row */} {/* Expanded detail */} {isExpanded && (
{/* Storage */}

Storage

{item.percentageFull != null && (

Fill level

{item.percentageFull}%
)}
{/* Chemical info */}

Chemical

{/* Procurement */}

Procurement

{item.expirationDate && (

Expiration

{item.expirationDate} {expired && ` (expired ${Math.abs(days!)}d ago)`} {expiringSoon && ` (${days}d left)`}

)} {item.comments && (

Comments

{item.comments}

)}
)} ); })}
)} {/* Add / Edit dialog */} {editingItem ? "Edit Chemical" : "Add Chemical"}
{/* Required fields */}

Required fields

setField("chemicalName", e.target.value)} />
setField("casNumber", e.target.value)} placeholder="e.g. 67-56-1" />
setField("piFirstName", e.target.value)} />
setField("bldgCode", e.target.value)} />
setField("lab", e.target.value)} />
setField("storageLocation", e.target.value)} placeholder="e.g. Cabinet A-3" />
setField("numberOfContainers", e.target.value)} />
setField("amountPerContainer", e.target.value)} placeholder="e.g. 500" />
{/* Optional fields toggle */} {showOptional && (

Optional fields

setField("chemicalFormula", e.target.value)} placeholder="e.g. CH₃OH" />
setField("molecularWeight", e.target.value)} placeholder="g/mol" />
setField("concentration", e.target.value)} placeholder="e.g. 99.8%" />
setField("percentageFull", e.target.value ? parseFloat(e.target.value) : undefined)} />
setField("vendor", e.target.value)} />
setField("catalogNumber", e.target.value)} />
setField("lotNumber", e.target.value)} />
setField("expirationDate", e.target.value)} />
setField("barcode", e.target.value)} />
setField("contact", e.target.value)} />
setField("comments", e.target.value)} />
)} {formError &&

{formError}

}
); }