initial
This commit is contained in:
716
components/Inventory.tsx
Normal file
716
components/Inventory.tsx
Normal file
@@ -0,0 +1,716 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user