added table view

This commit is contained in:
2026-04-02 00:40:43 +00:00
parent 39c7151e51
commit effacc4e87
3 changed files with 140 additions and 1 deletions

View File

@@ -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">