added table view
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
import { chemicalsApi } from "../lib/api";
|
||||
import type { ChemicalInventory } from "../shared/types";
|
||||
import { Card } from "./ui/card";
|
||||
@@ -9,6 +10,7 @@ import { Badge } from "./ui/badge";
|
||||
import {
|
||||
Camera, Sparkles, Mail, Search, Plus, AlertTriangle,
|
||||
ChevronDown, ChevronUp, Loader2, Trash2, FlaskConical,
|
||||
Table2, FileDown, FileSpreadsheet,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
|
||||
@@ -29,6 +31,40 @@ const physicalStates = ["Solid", "Liquid", "Gas"];
|
||||
|
||||
const unitOptions = ["mL", "L", "g", "kg", "mg", "oz", "lb", "gal", "mol"];
|
||||
|
||||
const TABLE_COLUMNS: { label: string; key: keyof ChemicalInventory }[] = [
|
||||
{ label: "PI First Name", key: "piFirstName" },
|
||||
{ label: "Physical State", key: "physicalState" },
|
||||
{ label: "Chemical Name", key: "chemicalName" },
|
||||
{ label: "Bldg Code", key: "bldgCode" },
|
||||
{ label: "LAB", key: "lab" },
|
||||
{ label: "Storage Location", key: "storageLocation" },
|
||||
{ label: "Storage Device", key: "storageDevice" },
|
||||
{ label: "# of Containers", key: "numberOfContainers" },
|
||||
{ label: "Amount per Container", key: "amountPerContainer" },
|
||||
{ label: "Unit of Measure", key: "unitOfMeasure" },
|
||||
{ label: "CAS #", key: "casNumber" },
|
||||
{ label: "Chemical Formula", key: "chemicalFormula" },
|
||||
{ label: "Molecular Weight", key: "molecularWeight" },
|
||||
{ label: "Vendor", key: "vendor" },
|
||||
{ label: "Catalog #", key: "catalogNumber" },
|
||||
{ label: "Found in Catalog", key: "foundInCatalog" },
|
||||
{ label: "PO#", key: "poNumber" },
|
||||
{ label: "Receipt Date", key: "receiptDate" },
|
||||
{ label: "Open Date", key: "openDate" },
|
||||
{ label: "MAX on Hand", key: "maxOnHand" },
|
||||
{ label: "Expiration Date", key: "expirationDate" },
|
||||
{ label: "Contact", key: "contact" },
|
||||
{ label: "Comments", key: "comments" },
|
||||
{ label: "Date Entered", key: "dateEntered" },
|
||||
{ label: "Permit #", key: "permitNumber" },
|
||||
{ label: "Barcode", key: "barcode" },
|
||||
{ label: "LAST_CHANGED", key: "lastChanged" },
|
||||
{ label: "Concentration", key: "concentration" },
|
||||
{ label: "Chemical Number", key: "chemicalNumber" },
|
||||
{ label: "Lot Number", key: "lotNumber" },
|
||||
{ label: "Multiple CAS (comma delimited)", key: "multipleCAS" },
|
||||
];
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function daysUntil(dateStr: string) {
|
||||
@@ -99,6 +135,9 @@ export function Inventory() {
|
||||
const [formError, setFormError] = useState("");
|
||||
const [showOptional, setShowOptional] = useState(false);
|
||||
|
||||
// table view dialog
|
||||
const [tableViewOpen, setTableViewOpen] = useState(false);
|
||||
|
||||
// scan dialog
|
||||
const [scanOpen, setScanOpen] = useState(false);
|
||||
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
||||
@@ -224,6 +263,36 @@ export function Inventory() {
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
|
||||
// ── export helpers ──────────────────────────────────────────────────────
|
||||
|
||||
function handleDownloadCSV() {
|
||||
const headers = TABLE_COLUMNS.map(c => c.label);
|
||||
const rows = filtered.map(item =>
|
||||
TABLE_COLUMNS.map(c => String(item[c.key] ?? ""))
|
||||
);
|
||||
const csv = [headers, ...rows]
|
||||
.map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(","))
|
||||
.join("\n");
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "chemical_inventory.csv";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
const wsData = [
|
||||
TABLE_COLUMNS.map(c => c.label),
|
||||
...filtered.map(item => TABLE_COLUMNS.map(c => item[c.key] ?? "")),
|
||||
];
|
||||
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Chemical Inventory");
|
||||
XLSX.writeFile(wb, "chemical_inventory.xlsx");
|
||||
}
|
||||
|
||||
// ── derived ─────────────────────────────────────────────────────────────
|
||||
|
||||
const filtered = inventory.filter(c =>
|
||||
@@ -247,6 +316,11 @@ export function Inventory() {
|
||||
<p className="text-muted-foreground text-sm">Track, manage, and monitor your lab chemicals</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{/* Table view */}
|
||||
<Button variant="outline" className="gap-2" onClick={() => setTableViewOpen(true)}>
|
||||
<Table2 className="w-4 h-4" /> Table View
|
||||
</Button>
|
||||
|
||||
{/* Add manually */}
|
||||
<Button onClick={openAdd} className="gap-2 bg-primary hover:bg-primary/90">
|
||||
<Plus className="w-4 h-4" /> Add Chemical
|
||||
@@ -527,6 +601,68 @@ export function Inventory() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table View dialog */}
|
||||
<Dialog open={tableViewOpen} onOpenChange={setTableViewOpen}>
|
||||
<DialogContent className="max-w-[95vw] w-[95vw] max-h-[90vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-5 pt-5 pb-3 shrink-0 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Table2 className="w-5 h-5" /> Inventory Table
|
||||
<span className="text-sm font-normal text-muted-foreground ml-1">
|
||||
({filtered.length} {filtered.length === 1 ? "chemical" : "chemicals"})
|
||||
</span>
|
||||
</DialogTitle>
|
||||
<div className="flex gap-2 mr-6">
|
||||
<Button size="sm" variant="outline" className="gap-1.5" onClick={handleDownloadCSV}>
|
||||
<FileDown className="w-4 h-4" /> CSV
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="gap-1.5" onClick={handleDownloadExcel}>
|
||||
<FileSpreadsheet className="w-4 h-4" /> Excel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="overflow-auto flex-1">
|
||||
<table className="text-xs border-collapse min-w-max">
|
||||
<thead className="sticky top-0 z-10 bg-muted">
|
||||
<tr>
|
||||
{TABLE_COLUMNS.map(col => (
|
||||
<th
|
||||
key={col.key}
|
||||
className="text-left px-3 py-2 font-semibold text-muted-foreground whitespace-nowrap border-b border-r border-border last:border-r-0"
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((item, i) => (
|
||||
<tr key={item.id} className={i % 2 === 0 ? "bg-background" : "bg-muted/30"}>
|
||||
{TABLE_COLUMNS.map(col => (
|
||||
<td
|
||||
key={col.key}
|
||||
className="px-3 py-1.5 whitespace-nowrap border-b border-r border-border/50 last:border-r-0 max-w-[200px] overflow-hidden text-ellipsis"
|
||||
title={String(item[col.key] ?? "")}
|
||||
>
|
||||
{String(item[col.key] ?? "")}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={TABLE_COLUMNS.length} className="text-center py-10 text-muted-foreground">
|
||||
No chemicals to display.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add / Edit dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
|
||||
Reference in New Issue
Block a user