Files
LabWise/components/Inventory.tsx

688 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ChemicalInventory> {
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 (
<div>
<p className={`text-xs mb-0.5 ${required ? "text-red-500" : "text-muted-foreground"}`}>{label}</p>
<p className="text-sm text-foreground">{value}</p>
</div>
);
}
// ── main component ─────────────────────────────────────────────────────────
export function Inventory() {
const [inventory, setInventory] = useState<ChemicalInventory[]>([]);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [expandedId, setExpandedId] = useState<string | null>(null);
// add/edit dialog
const [dialogOpen, setDialogOpen] = useState(false);
const [editingItem, setEditingItem] = useState<ChemicalInventory | null>(null);
const [form, setForm] = useState<Partial<ChemicalInventory>>(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<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [extractedData, setExtractedData] = useState<Partial<ChemicalInventory> | null>(null);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) {
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 (
<div className="p-6 max-w-5xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-foreground mb-1">Chemical Inventory</h1>
<p className="text-muted-foreground text-sm">Track, manage, and monitor your lab chemicals</p>
</div>
<div className="flex gap-2">
{/* Add manually */}
<Button onClick={openAdd} className="gap-2 bg-primary hover:bg-primary/90">
<Plus className="w-4 h-4" /> Add Chemical
</Button>
{/* Scan */}
<Dialog open={scanOpen} onOpenChange={setScanOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="gap-2">
<Camera className="w-4 h-4" /> Scan Label
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Camera className="w-5 h-5" /> Scan Chemical Label
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
{!capturedImage ? (
<div
className="border-2 border-dashed border-border rounded-lg p-12 text-center hover:border-primary transition-colors cursor-pointer bg-muted/30"
onClick={() => fileInputRef.current?.click()}
>
<Camera className="w-14 h-14 text-muted-foreground mx-auto mb-3" />
<p className="text-foreground mb-1">Click to capture or upload a label photo</p>
<p className="text-muted-foreground text-sm">AI will extract all available information</p>
<input ref={fileInputRef} type="file" accept="image/*" capture="environment"
className="hidden" onChange={handlePhotoCapture} />
</div>
) : (
<div className="space-y-4">
<img src={capturedImage} alt="Scanned label" className="w-full rounded-lg border border-border" />
{isProcessing ? (
<div className="bg-accent border border-border rounded-lg p-6 text-center">
<Sparkles className="w-8 h-8 text-primary mx-auto mb-2 animate-pulse" />
<p className="text-foreground">Scanning label</p>
<p className="text-muted-foreground text-sm">Reading all visible information with AI</p>
</div>
) : extractedData ? (
<div className="space-y-4">
<div className="bg-accent border border-border rounded-lg p-3 flex items-start gap-2">
<Sparkles className="w-4 h-4 text-primary mt-0.5" />
<p className="text-sm text-foreground">Information extracted. Yellow fields need manual entry.</p>
</div>
<div className="grid grid-cols-2 gap-3">
{(["piFirstName","bldgCode","lab","storageLocation"] as const).map(k => (
<div key={k} className={`space-y-1 ${extractedData.needsManualEntry?.includes(k) ? "bg-yellow-50 p-2 rounded" : ""}`}>
<Label className="text-red-600 text-xs">{k === "piFirstName" ? "PI First Name" : k === "bldgCode" ? "Building Code" : k === "lab" ? "Lab" : "Storage Location"} *</Label>
<Input value={(extractedData as Record<string,string>)[k] || ""} onChange={e => setExtractedData({ ...extractedData, [k]: e.target.value })} />
</div>
))}
<div className="space-y-1">
<Label className="text-red-600 text-xs">Chemical Name *</Label>
<Input value={extractedData.chemicalName || ""} onChange={e => setExtractedData({...extractedData, chemicalName: e.target.value})} />
</div>
<div className="space-y-1">
<Label className="text-red-600 text-xs">CAS # *</Label>
<Input value={extractedData.casNumber || ""} onChange={e => setExtractedData({...extractedData, casNumber: e.target.value})} />
</div>
<div className="space-y-1">
<Label className="text-red-600 text-xs">Physical State *</Label>
<Select value={extractedData.physicalState} onValueChange={v => setExtractedData({...extractedData, physicalState: v})}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{physicalStates.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-red-600 text-xs">Storage Device *</Label>
<Select value={extractedData.storageDevice} onValueChange={v => setExtractedData({...extractedData, storageDevice: v})}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{storageDeviceOptions.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Amount / Container</Label>
<Input value={extractedData.amountPerContainer || ""} onChange={e => setExtractedData({...extractedData, amountPerContainer: e.target.value})} />
</div>
<div className="space-y-1">
<Label className="text-xs">Unit</Label>
<Input value={extractedData.unitOfMeasure || ""} onChange={e => setExtractedData({...extractedData, unitOfMeasure: e.target.value})} />
</div>
<div className="space-y-1">
<Label className="text-xs">Expiration Date</Label>
<Input type="date" value={extractedData.expirationDate || ""} onChange={e => setExtractedData({...extractedData, expirationDate: e.target.value})} />
</div>
<div className="space-y-1">
<Label className="text-xs text-blue-600">% Full</Label>
<Input type="number" min={0} max={100} value={extractedData.percentageFull ?? ""} onChange={e => setExtractedData({...extractedData, percentageFull: parseFloat(e.target.value)})} />
</div>
</div>
<div className="flex gap-2">
<Button className="flex-1 bg-primary hover:bg-primary/90" onClick={handleAddFromScan}>Add to Inventory</Button>
<Button variant="outline" onClick={() => { setCapturedImage(null); setExtractedData(null); }}>Retake</Button>
</div>
</div>
) : null}
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
</div>
{/* Email receipt banner */}
<Card className="p-4 mb-5 bg-gradient-to-r from-accent to-secondary border-border">
<div className="flex items-start gap-3">
<Mail className="w-4 h-4 text-primary mt-0.5 shrink-0" />
<p className="text-sm text-foreground">
Auto-import receipts: email them to{" "}
<code className="bg-muted px-1.5 py-0.5 rounded text-primary text-xs">inventory@labwise-auto.com</code>
{" "}and we'll extract and add chemicals automatically.
</p>
</div>
</Card>
{/* Search + stats */}
<div className="flex items-center gap-3 mb-5">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Search by name, CAS, lab, or location…"
className="pl-9"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 shrink-0 text-sm text-muted-foreground">
<FlaskConical className="w-4 h-4 text-[#5a9584]" />
<span>{inventory.length} chemicals</span>
{lowStockCount > 0 && (
<>
<span className="text-border">·</span>
<AlertTriangle className="w-4 h-4 text-amber-500" />
<span className="text-amber-600">{lowStockCount} low stock</span>
</>
)}
</div>
</div>
{/* List */}
{isLoading ? (
<div className="flex justify-center py-16">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
) : filtered.length === 0 ? (
<div className="text-center py-16 text-muted-foreground">
{inventory.length === 0
? "No chemicals yet. Add one to get started."
: "No chemicals match your search."}
</div>
) : (
<div className="space-y-2">
{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 (
<Card key={item.id} className={`overflow-hidden transition-shadow ${isExpanded ? "shadow-md" : "hover:shadow-sm"}`}>
{/* Summary row */}
<button
className="w-full text-left px-5 py-4 flex items-center gap-4"
onClick={() => setExpandedId(isExpanded ? null : item.id)}
>
{/* Name + state */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-foreground">{item.chemicalName}</span>
<span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">{item.physicalState}</span>
{expired && <Badge className="bg-red-100 text-red-700 border-red-200 border text-xs py-0">Expired</Badge>}
{expiringSoon && !expired && <Badge className="bg-amber-100 text-amber-700 border-amber-200 border text-xs py-0">Exp. {days}d</Badge>}
{lowStock && <Badge className="bg-red-100 text-red-700 border-red-200 border text-xs py-0">Low stock</Badge>}
</div>
<p className="text-sm text-muted-foreground mt-0.5">{item.lab} · {item.storageLocation || "—"}</p>
</div>
{/* Amount */}
<div className="text-sm text-muted-foreground shrink-0 hidden sm:block">
{item.numberOfContainers} × {item.amountPerContainer} {item.unitOfMeasure}
</div>
{/* Fill bar */}
{item.percentageFull != null && (
<div className="flex items-center gap-2 shrink-0 hidden md:flex">
<div className="w-20 h-1.5 bg-muted rounded-full overflow-hidden">
<div className={`h-full rounded-full ${fillBarColor(item.percentageFull)}`} style={{ width: `${item.percentageFull}%` }} />
</div>
<span className="text-xs text-muted-foreground w-8">{item.percentageFull}%</span>
</div>
)}
{/* Chevron */}
{isExpanded
? <ChevronUp className="w-4 h-4 text-muted-foreground shrink-0" />
: <ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />}
</button>
{/* Expanded detail */}
{isExpanded && (
<div className="border-t border-border px-5 py-4 bg-secondary/40">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-5">
{/* Storage */}
<div className="space-y-3">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Storage</p>
<Field label="Building" value={item.bldgCode} required />
<Field label="Lab" value={item.lab} required />
<Field label="Location" value={item.storageLocation} required />
<Field label="Device" value={item.storageDevice} required />
<Field label="Containers" value={item.numberOfContainers} required />
<Field label="Amount / Container" value={`${item.amountPerContainer} ${item.unitOfMeasure}`} required />
{item.percentageFull != null && (
<div>
<p className="text-xs text-muted-foreground mb-1">Fill level</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div className={`h-full rounded-full ${fillBarColor(item.percentageFull)}`} style={{ width: `${item.percentageFull}%` }} />
</div>
<span className="text-sm font-medium">{item.percentageFull}%</span>
</div>
</div>
)}
</div>
{/* Chemical info */}
<div className="space-y-3">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Chemical</p>
<Field label="CAS #" value={item.casNumber} required />
<Field label="Physical State" value={item.physicalState} required />
<Field label="Formula" value={item.chemicalFormula} />
<Field label="Molecular Weight" value={item.molecularWeight ? `${item.molecularWeight} g/mol` : undefined} />
<Field label="Concentration" value={item.concentration} />
<Field label="PI" value={item.piFirstName} />
<Field label="Contact" value={item.contact} />
</div>
{/* Procurement */}
<div className="space-y-3">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Procurement</p>
<Field label="Vendor" value={item.vendor} />
<Field label="Catalog #" value={item.catalogNumber} />
<Field label="Lot #" value={item.lotNumber} />
{item.expirationDate && (
<div>
<p className="text-xs text-muted-foreground mb-0.5">Expiration</p>
<p className={`text-sm font-medium ${expired ? "text-red-600" : expiringSoon ? "text-amber-600" : "text-foreground"}`}>
{item.expirationDate}
{expired && ` (expired ${Math.abs(days!)}d ago)`}
{expiringSoon && ` (${days}d left)`}
</p>
</div>
)}
<Field label="Barcode" value={item.barcode} />
{item.comments && (
<div>
<p className="text-xs text-muted-foreground mb-0.5">Comments</p>
<p className="text-sm text-foreground whitespace-pre-wrap">{item.comments}</p>
</div>
)}
</div>
</div>
<div className="flex gap-2 pt-2 border-t border-border">
<Button size="sm" variant="outline" onClick={() => openEdit(item)}>Edit</Button>
<Button size="sm" variant="ghost" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => handleDelete(item.id)}>
<Trash2 className="w-4 h-4 mr-1" /> Delete
</Button>
</div>
</div>
)}
</Card>
);
})}
</div>
)}
{/* Add / Edit dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingItem ? "Edit Chemical" : "Add Chemical"}</DialogTitle>
</DialogHeader>
<div className="space-y-5 py-2">
{/* Required fields */}
<div>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">Required fields</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">Chemical Name <span className="text-red-500">*</span></Label>
<Input value={form.chemicalName || ""} onChange={e => setField("chemicalName", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">CAS # <span className="text-red-500">*</span></Label>
<Input value={form.casNumber || ""} onChange={e => setField("casNumber", e.target.value)} placeholder="e.g. 67-56-1" />
</div>
<div className="space-y-1">
<Label className="text-xs">Physical State <span className="text-red-500">*</span></Label>
<Select value={form.physicalState || ""} onValueChange={v => setField("physicalState", v)}>
<SelectTrigger><SelectValue placeholder="Select…" /></SelectTrigger>
<SelectContent>{physicalStates.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Storage Device <span className="text-red-500">*</span></Label>
<Select value={form.storageDevice || ""} onValueChange={v => setField("storageDevice", v)}>
<SelectTrigger><SelectValue placeholder="Select…" /></SelectTrigger>
<SelectContent>{storageDeviceOptions.map(s => <SelectItem key={s} value={s}>{s}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">PI First Name <span className="text-red-500">*</span></Label>
<Input value={form.piFirstName || ""} onChange={e => setField("piFirstName", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Building Code <span className="text-red-500">*</span></Label>
<Input value={form.bldgCode || ""} onChange={e => setField("bldgCode", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Lab <span className="text-red-500">*</span></Label>
<Input value={form.lab || ""} onChange={e => setField("lab", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Storage Location <span className="text-red-500">*</span></Label>
<Input value={form.storageLocation || ""} onChange={e => setField("storageLocation", e.target.value)} placeholder="e.g. Cabinet A-3" />
</div>
<div className="space-y-1">
<Label className="text-xs"># of Containers <span className="text-red-500">*</span></Label>
<Input type="number" min={1} value={form.numberOfContainers || ""} onChange={e => setField("numberOfContainers", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Amount / Container <span className="text-red-500">*</span></Label>
<Input value={form.amountPerContainer || ""} onChange={e => setField("amountPerContainer", e.target.value)} placeholder="e.g. 500" />
</div>
<div className="space-y-1 col-span-2">
<Label className="text-xs">Unit of Measure <span className="text-red-500">*</span></Label>
<Select value={form.unitOfMeasure || ""} onValueChange={v => setField("unitOfMeasure", v)}>
<SelectTrigger><SelectValue placeholder="Select…" /></SelectTrigger>
<SelectContent>
{unitOptions.map(u => <SelectItem key={u} value={u}>{u}</SelectItem>)}
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Optional fields toggle */}
<button
type="button"
className="flex items-center gap-1.5 text-sm text-primary hover:underline"
onClick={() => setShowOptional(v => !v)}
>
{showOptional ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
{showOptional ? "Hide" : "Show"} optional fields
</button>
{showOptional && (
<div>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">Optional fields</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">Chemical Formula</Label>
<Input value={form.chemicalFormula || ""} onChange={e => setField("chemicalFormula", e.target.value)} placeholder="e.g. CH₃OH" />
</div>
<div className="space-y-1">
<Label className="text-xs">Molecular Weight</Label>
<Input value={form.molecularWeight || ""} onChange={e => setField("molecularWeight", e.target.value)} placeholder="g/mol" />
</div>
<div className="space-y-1">
<Label className="text-xs">Concentration</Label>
<Input value={form.concentration || ""} onChange={e => setField("concentration", e.target.value)} placeholder="e.g. 99.8%" />
</div>
<div className="space-y-1">
<Label className="text-xs">% Full</Label>
<Input type="number" min={0} max={100} value={form.percentageFull ?? ""} onChange={e => setField("percentageFull", e.target.value ? parseFloat(e.target.value) : undefined)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Vendor</Label>
<Input value={form.vendor || ""} onChange={e => setField("vendor", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Catalog #</Label>
<Input value={form.catalogNumber || ""} onChange={e => setField("catalogNumber", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Lot #</Label>
<Input value={form.lotNumber || ""} onChange={e => setField("lotNumber", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Expiration Date</Label>
<Input type="date" value={form.expirationDate || ""} onChange={e => setField("expirationDate", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Barcode</Label>
<Input value={form.barcode || ""} onChange={e => setField("barcode", e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs">Contact</Label>
<Input value={form.contact || ""} onChange={e => setField("contact", e.target.value)} />
</div>
<div className="space-y-1 col-span-2">
<Label className="text-xs">Comments</Label>
<Input value={form.comments || ""} onChange={e => setField("comments", e.target.value)} />
</div>
</div>
</div>
)}
{formError && <p className="text-sm text-red-600">{formError}</p>}
<div className="flex gap-2 pt-1">
<Button className="flex-1 bg-primary hover:bg-primary/90" onClick={handleSave} disabled={isSaving}>
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : editingItem ? "Save Changes" : "Add to Inventory"}
</Button>
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}