717 lines
33 KiB
TypeScript
717 lines
33 KiB
TypeScript
import { useState, useRef } from "react";
|
|
import { Card } from "./ui/card";
|
|
import { Button } from "./ui/button";
|
|
import { Input } from "./ui/input";
|
|
import { Badge } from "./ui/badge";
|
|
import {
|
|
Camera,
|
|
Sparkles,
|
|
Mail,
|
|
Users,
|
|
Download,
|
|
Upload,
|
|
Search,
|
|
Filter,
|
|
Plus,
|
|
AlertTriangle,
|
|
Check,
|
|
X
|
|
} from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "./ui/dialog";
|
|
import { Label } from "./ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "./ui/select";
|
|
|
|
interface ChemicalInventory {
|
|
id: string;
|
|
// Required fields (red)
|
|
piFirstName: string;
|
|
physicalState: string;
|
|
chemicalName: string;
|
|
bldgCode: string;
|
|
lab: string;
|
|
storageLocation: string;
|
|
storageDevice: string;
|
|
numberOfContainers: string;
|
|
amountPerContainer: string;
|
|
unitOfMeasure: string;
|
|
casNumber: string;
|
|
// Nice to have fields
|
|
chemicalFormula?: string;
|
|
molecularWeight?: string;
|
|
vendor?: string;
|
|
catalogNumber?: string;
|
|
foundInCatalog?: string;
|
|
poNumber?: string;
|
|
receiptDate?: string;
|
|
openDate?: string;
|
|
maxOnHand?: string;
|
|
expirationDate?: string;
|
|
contact?: string;
|
|
comments?: string;
|
|
dateEntered?: string;
|
|
permitNumber?: string;
|
|
barcode?: string;
|
|
lastChanged?: string;
|
|
concentration?: string;
|
|
chemicalNumber?: string;
|
|
lotNumber?: string;
|
|
multipleCAS?: string;
|
|
msds?: string;
|
|
// Special fields
|
|
percentageFull?: number; // blue
|
|
needsManualEntry?: string[]; // yellow highlight
|
|
scannedImage?: string;
|
|
}
|
|
|
|
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"
|
|
];
|
|
|
|
export function Inventory() {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [isPhotoDialogOpen, setIsPhotoDialogOpen] = useState(false);
|
|
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [extractedData, setExtractedData] = useState<Partial<ChemicalInventory> | null>(null);
|
|
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [inventory, setInventory] = useState<ChemicalInventory[]>([
|
|
{
|
|
id: "1",
|
|
piFirstName: "Dr. Smith",
|
|
physicalState: "Liquid",
|
|
chemicalName: "Acetone",
|
|
bldgCode: "BLD-A",
|
|
lab: "Lab 201",
|
|
storageLocation: "Cabinet A-3",
|
|
storageDevice: "Glass Bottle",
|
|
numberOfContainers: "2",
|
|
amountPerContainer: "1",
|
|
unitOfMeasure: "L",
|
|
casNumber: "67-64-1",
|
|
chemicalFormula: "C3H6O",
|
|
molecularWeight: "58.08",
|
|
vendor: "Sigma-Aldrich",
|
|
catalogNumber: "179124",
|
|
expirationDate: "2025-06-15",
|
|
lotNumber: "SLCD1234",
|
|
percentageFull: 65,
|
|
dateEntered: "2024-11-15",
|
|
openDate: "2024-11-15"
|
|
},
|
|
{
|
|
id: "2",
|
|
piFirstName: "Dr. Johnson",
|
|
physicalState: "Solid",
|
|
chemicalName: "Sodium Hydroxide",
|
|
bldgCode: "BLD-B",
|
|
lab: "Lab 305",
|
|
storageLocation: "Cabinet B-1",
|
|
storageDevice: "Plastic Bottle",
|
|
numberOfContainers: "1",
|
|
amountPerContainer: "500",
|
|
unitOfMeasure: "g",
|
|
casNumber: "1310-73-2",
|
|
chemicalFormula: "NaOH",
|
|
molecularWeight: "39.997",
|
|
vendor: "Fisher Scientific",
|
|
expirationDate: "2025-10-22",
|
|
lotNumber: "FS9876",
|
|
percentageFull: 15,
|
|
dateEntered: "2024-10-22"
|
|
},
|
|
{
|
|
id: "3",
|
|
piFirstName: "Dr. Smith",
|
|
physicalState: "Liquid",
|
|
chemicalName: "Hydrochloric Acid",
|
|
bldgCode: "BLD-A",
|
|
lab: "Lab 201",
|
|
storageLocation: "Acid Cabinet",
|
|
storageDevice: "Glass Bottle",
|
|
numberOfContainers: "1",
|
|
amountPerContainer: "1",
|
|
unitOfMeasure: "L",
|
|
casNumber: "7647-01-0",
|
|
chemicalFormula: "HCl",
|
|
vendor: "Fisher Scientific",
|
|
expirationDate: "2024-12-10",
|
|
percentageFull: 80,
|
|
dateEntered: "2024-08-15"
|
|
}
|
|
]);
|
|
|
|
const handlePhotoCapture = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setCapturedImage(reader.result as string);
|
|
processImage(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const processImage = (imageData: string) => {
|
|
setIsProcessing(true);
|
|
|
|
// Simulate AI processing with OCR
|
|
setTimeout(() => {
|
|
const mockExtractedData: Partial<ChemicalInventory> = {
|
|
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, // Scanner detected transparency
|
|
// Fields that need manual entry
|
|
needsManualEntry: ["piFirstName", "bldgCode", "lab", "storageLocation"],
|
|
scannedImage: imageData
|
|
};
|
|
|
|
setExtractedData(mockExtractedData);
|
|
setIsProcessing(false);
|
|
}, 2000);
|
|
};
|
|
|
|
const handleAddFromScan = () => {
|
|
if (extractedData) {
|
|
const newChemical: ChemicalInventory = {
|
|
id: Date.now().toString(),
|
|
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 || "",
|
|
chemicalFormula: extractedData.chemicalFormula,
|
|
molecularWeight: extractedData.molecularWeight,
|
|
vendor: extractedData.vendor,
|
|
catalogNumber: extractedData.catalogNumber,
|
|
expirationDate: extractedData.expirationDate,
|
|
lotNumber: extractedData.lotNumber,
|
|
concentration: extractedData.concentration,
|
|
percentageFull: extractedData.percentageFull,
|
|
dateEntered: new Date().toISOString().split('T')[0],
|
|
needsManualEntry: extractedData.needsManualEntry,
|
|
scannedImage: extractedData.scannedImage
|
|
};
|
|
|
|
setInventory([newChemical, ...inventory]);
|
|
setCapturedImage(null);
|
|
setExtractedData(null);
|
|
setIsPhotoDialogOpen(false);
|
|
}
|
|
};
|
|
|
|
const filteredInventory = inventory.filter(item =>
|
|
item.chemicalName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
item.casNumber.includes(searchQuery) ||
|
|
item.piFirstName.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
const lowStockChemicals = inventory.filter(item =>
|
|
item.percentageFull !== undefined && item.percentageFull < 20
|
|
);
|
|
|
|
const expiringChemicals = inventory.filter(item => {
|
|
if (!item.expirationDate) return false;
|
|
const today = new Date();
|
|
const expDate = new Date(item.expirationDate);
|
|
const daysUntilExpiry = Math.floor((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
return daysUntilExpiry <= 30 || daysUntilExpiry < 0;
|
|
});
|
|
|
|
const requiredFields = [
|
|
"piFirstName", "physicalState", "chemicalName", "bldgCode", "lab",
|
|
"storageLocation", "storageDevice", "numberOfContainers",
|
|
"amountPerContainer", "unitOfMeasure", "casNumber"
|
|
];
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="max-w-[1800px] mx-auto">
|
|
{/* Header */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h1 className="text-foreground mb-2">Chemical Inventory</h1>
|
|
<p className="text-muted-foreground">Collaborative inventory tracking with automated scanning</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" className="gap-2">
|
|
<Users className="w-4 h-4" />
|
|
Share Access
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Share Inventory Access</DialogTitle>
|
|
<DialogDescription>
|
|
Give students and lab members access to this inventory
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email Address</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="student@university.edu"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="role">Access Level</Label>
|
|
<Select defaultValue="editor">
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="viewer">Viewer (Read-only)</SelectItem>
|
|
<SelectItem value="editor">Editor (Can add/scan)</SelectItem>
|
|
<SelectItem value="admin">Admin (Full access)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="bg-accent p-4 rounded-lg border border-border">
|
|
<h4 className="text-foreground mb-2">Current Access</h4>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-foreground">Dr. Smith (You)</span>
|
|
<Badge variant="outline">Owner</Badge>
|
|
</div>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-foreground">Lab Assistant</span>
|
|
<Badge variant="outline">Editor</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button className="w-full bg-primary hover:bg-primary/90">
|
|
Send Invitation
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Button variant="outline" className="gap-2">
|
|
<Download className="w-4 h-4" />
|
|
Export CSV
|
|
</Button>
|
|
|
|
<Dialog open={isPhotoDialogOpen} onOpenChange={setIsPhotoDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-primary hover:bg-primary/90 gap-2">
|
|
<Camera className="w-4 h-4" />
|
|
Scan Chemical
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Camera className="w-5 h-5" />
|
|
Scan Chemical Label
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Automatically extract information from chemical labels
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
{!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-16 h-16 text-muted-foreground mx-auto mb-4" />
|
|
<p className="text-foreground mb-2">Click to capture or upload label photo</p>
|
|
<p className="text-muted-foreground">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">
|
|
<div className="relative rounded-lg overflow-hidden border border-border">
|
|
<img src={capturedImage} alt="Scanned label" className="w-full h-auto" />
|
|
</div>
|
|
|
|
{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-3 animate-pulse" />
|
|
<p className="text-foreground mb-1">Scanning label...</p>
|
|
<p className="text-muted-foreground">Reading all visible information with AI</p>
|
|
</div>
|
|
) : extractedData ? (
|
|
<div className="space-y-4">
|
|
<div className="bg-accent border border-border rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Sparkles className="w-5 h-5 text-primary mt-0.5" />
|
|
<div>
|
|
<p className="text-foreground mb-1">Information Extracted!</p>
|
|
<p className="text-muted-foreground">
|
|
Yellow fields need manual entry. Review and complete before adding.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{/* Required Fields */}
|
|
<div className={`space-y-2 ${extractedData.needsManualEntry?.includes('piFirstName') ? 'bg-yellow-50 p-2 rounded' : ''}`}>
|
|
<Label className="text-red-600">PI First Name *</Label>
|
|
<Input
|
|
value={extractedData.piFirstName || ""}
|
|
onChange={(e) => setExtractedData({ ...extractedData, piFirstName: e.target.value })}
|
|
placeholder="Required"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">Physical State *</Label>
|
|
<Select
|
|
value={extractedData.physicalState}
|
|
onValueChange={(value) => setExtractedData({ ...extractedData, physicalState: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select state" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Solid">Solid</SelectItem>
|
|
<SelectItem value="Liquid">Liquid</SelectItem>
|
|
<SelectItem value="Gas">Gas</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">Chemical Name *</Label>
|
|
<Input value={extractedData.chemicalName || ""} onChange={(e) => setExtractedData({ ...extractedData, chemicalName: e.target.value })} />
|
|
</div>
|
|
|
|
<div className={`space-y-2 ${extractedData.needsManualEntry?.includes('bldgCode') ? 'bg-yellow-50 p-2 rounded' : ''}`}>
|
|
<Label className="text-red-600">Bldg Code *</Label>
|
|
<Input value={extractedData.bldgCode || ""} onChange={(e) => setExtractedData({ ...extractedData, bldgCode: e.target.value })} placeholder="Required" />
|
|
</div>
|
|
|
|
<div className={`space-y-2 ${extractedData.needsManualEntry?.includes('lab') ? 'bg-yellow-50 p-2 rounded' : ''}`}>
|
|
<Label className="text-red-600">LAB *</Label>
|
|
<Input value={extractedData.lab || ""} onChange={(e) => setExtractedData({ ...extractedData, lab: e.target.value })} placeholder="Required" />
|
|
</div>
|
|
|
|
<div className={`space-y-2 ${extractedData.needsManualEntry?.includes('storageLocation') ? 'bg-yellow-50 p-2 rounded' : ''}`}>
|
|
<Label className="text-red-600">Storage Location *</Label>
|
|
<Input value={extractedData.storageLocation || ""} onChange={(e) => setExtractedData({ ...extractedData, storageLocation: e.target.value })} placeholder="Required" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">Storage Device *</Label>
|
|
<Select
|
|
value={extractedData.storageDevice}
|
|
onValueChange={(value) => setExtractedData({ ...extractedData, storageDevice: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{storageDeviceOptions.map(option => (
|
|
<SelectItem key={option} value={option}>{option}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600"># of Containers *</Label>
|
|
<Input type="number" value={extractedData.numberOfContainers || ""} onChange={(e) => setExtractedData({ ...extractedData, numberOfContainers: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">Amount Per Container *</Label>
|
|
<Input value={extractedData.amountPerContainer || ""} onChange={(e) => setExtractedData({ ...extractedData, amountPerContainer: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">Unit of Measure *</Label>
|
|
<Input value={extractedData.unitOfMeasure || ""} onChange={(e) => setExtractedData({ ...extractedData, unitOfMeasure: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-red-600">CAS # *</Label>
|
|
<Input value={extractedData.casNumber || ""} onChange={(e) => setExtractedData({ ...extractedData, casNumber: e.target.value })} />
|
|
</div>
|
|
|
|
{/* Optional Fields */}
|
|
<div className="space-y-2">
|
|
<Label>Chemical Formula</Label>
|
|
<Input value={extractedData.chemicalFormula || ""} onChange={(e) => setExtractedData({ ...extractedData, chemicalFormula: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Molecular Weight</Label>
|
|
<Input value={extractedData.molecularWeight || ""} onChange={(e) => setExtractedData({ ...extractedData, molecularWeight: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Vendor</Label>
|
|
<Input value={extractedData.vendor || ""} onChange={(e) => setExtractedData({ ...extractedData, vendor: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Catalog #</Label>
|
|
<Input value={extractedData.catalogNumber || ""} onChange={(e) => setExtractedData({ ...extractedData, catalogNumber: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Lot Number</Label>
|
|
<Input value={extractedData.lotNumber || ""} onChange={(e) => setExtractedData({ ...extractedData, lotNumber: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Expiration Date</Label>
|
|
<Input type="date" value={extractedData.expirationDate || ""} onChange={(e) => setExtractedData({ ...extractedData, expirationDate: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="space-y-2 bg-blue-50 p-2 rounded">
|
|
<Label className="text-blue-600">Percentage Full</Label>
|
|
<Input
|
|
type="number"
|
|
value={extractedData.percentageFull || ""}
|
|
onChange={(e) => setExtractedData({ ...extractedData, percentageFull: parseFloat(e.target.value) })}
|
|
placeholder="Auto-detected for transparent bottles"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Concentration</Label>
|
|
<Input value={extractedData.concentration || ""} onChange={(e) => setExtractedData({ ...extractedData, concentration: 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 Info */}
|
|
<Card className="p-4 bg-gradient-to-r from-accent to-secondary border-border">
|
|
<div className="flex items-start gap-3">
|
|
<Mail className="w-5 h-5 text-primary mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="text-foreground mb-1">
|
|
<span className="">Auto-import chemical receipts:</span> Email receipts to{" "}
|
|
<code className="bg-muted px-2 py-0.5 rounded text-primary">inventory@labwise-auto.com</code>
|
|
</p>
|
|
<p className="text-muted-foreground">
|
|
We'll automatically extract and add chemicals to your inventory
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Search and Stats */}
|
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
|
<Card className="col-span-2 p-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-5 h-5" />
|
|
<Input
|
|
placeholder="Search by chemical name, CAS, or PI..."
|
|
className="pl-10"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-muted-foreground">Total Chemicals</p>
|
|
<p className="text-foreground">{inventory.length}</p>
|
|
</div>
|
|
<AlertTriangle className="w-8 h-8 text-primary" />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="p-4 bg-amber-50 border-amber-200">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-amber-700">Low Stock</p>
|
|
<p className="text-foreground">{lowStockChemicals.length}</p>
|
|
</div>
|
|
<AlertTriangle className="w-8 h-8 text-amber-600" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Spreadsheet Table */}
|
|
<Card className="overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted border-b border-border sticky top-0">
|
|
<tr>
|
|
{/* Required columns - red headers */}
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">PI First Name</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Physical State</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Chemical Name</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Bldg Code</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">LAB</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Storage Location</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Storage Device</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200"># Containers</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Amount/Container</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">Unit</th>
|
|
<th className="text-left p-3 text-red-600 bg-red-50 border-r border-red-200">CAS #</th>
|
|
{/* Blue column */}
|
|
<th className="text-left p-3 text-blue-600 bg-blue-50 border-r border-blue-200">% Full</th>
|
|
{/* Optional columns */}
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Formula</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Mol. Weight</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Vendor</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Catalog #</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Expiration</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Lot #</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Concentration</th>
|
|
<th className="text-left p-3 text-muted-foreground border-r border-border">Date Entered</th>
|
|
<th className="text-left p-3 text-muted-foreground">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredInventory.map((item) => (
|
|
<tr key={item.id} className="border-b border-border hover:bg-accent/50">
|
|
{/* Required fields with yellow highlight if needed */}
|
|
<td className={`p-3 border-r border-border ${item.needsManualEntry?.includes('piFirstName') ? 'bg-yellow-100' : ''}`}>
|
|
{item.piFirstName || <span className="text-muted-foreground italic">Missing</span>}
|
|
</td>
|
|
<td className="p-3 border-r border-border">{item.physicalState}</td>
|
|
<td className="p-3 border-r border-border">{item.chemicalName}</td>
|
|
<td className={`p-3 border-r border-border ${item.needsManualEntry?.includes('bldgCode') ? 'bg-yellow-100' : ''}`}>
|
|
{item.bldgCode || <span className="text-muted-foreground italic">Missing</span>}
|
|
</td>
|
|
<td className={`p-3 border-r border-border ${item.needsManualEntry?.includes('lab') ? 'bg-yellow-100' : ''}`}>
|
|
{item.lab || <span className="text-muted-foreground italic">Missing</span>}
|
|
</td>
|
|
<td className={`p-3 border-r border-border ${item.needsManualEntry?.includes('storageLocation') ? 'bg-yellow-100' : ''}`}>
|
|
{item.storageLocation || <span className="text-muted-foreground italic">Missing</span>}
|
|
</td>
|
|
<td className="p-3 border-r border-border">{item.storageDevice}</td>
|
|
<td className="p-3 border-r border-border">{item.numberOfContainers}</td>
|
|
<td className="p-3 border-r border-border">{item.amountPerContainer}</td>
|
|
<td className="p-3 border-r border-border">{item.unitOfMeasure}</td>
|
|
<td className="p-3 border-r border-border">{item.casNumber}</td>
|
|
{/* Blue percentage full column */}
|
|
<td className="p-3 border-r border-blue-200 bg-blue-50">
|
|
{item.percentageFull !== undefined ? (
|
|
<div className="flex items-center gap-2">
|
|
<span className={item.percentageFull < 20 ? 'text-red-600' : 'text-blue-600'}>
|
|
{item.percentageFull}%
|
|
</span>
|
|
{item.percentageFull < 20 && <AlertTriangle className="w-4 h-4 text-red-600" />}
|
|
</div>
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</td>
|
|
{/* Optional fields */}
|
|
<td className="p-3 border-r border-border">{item.chemicalFormula || "-"}</td>
|
|
<td className="p-3 border-r border-border">{item.molecularWeight || "-"}</td>
|
|
<td className="p-3 border-r border-border">{item.vendor || "-"}</td>
|
|
<td className="p-3 border-r border-border">{item.catalogNumber || "-"}</td>
|
|
<td className="p-3 border-r border-border">
|
|
{item.expirationDate ? (
|
|
<span className={
|
|
new Date(item.expirationDate) < new Date() ? 'text-red-600' :
|
|
Math.floor((new Date(item.expirationDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) <= 30 ? 'text-amber-600' :
|
|
'text-foreground'
|
|
}>
|
|
{item.expirationDate}
|
|
</span>
|
|
) : "-"}
|
|
</td>
|
|
<td className="p-3 border-r border-border">{item.lotNumber || "-"}</td>
|
|
<td className="p-3 border-r border-border">{item.concentration || "-"}</td>
|
|
<td className="p-3 border-r border-border">{item.dateEntered || "-"}</td>
|
|
<td className="p-3">
|
|
<Button variant="ghost" size="sm">Edit</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|