Dashboard allows for manual inventory updates and shows relevant info.
This commit is contained in:
@@ -1,19 +1,21 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Card } from "./ui/card";
|
import { Card } from "./ui/card";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
Clock,
|
||||||
Clock,
|
|
||||||
TrendingUp,
|
|
||||||
Package,
|
Package,
|
||||||
FileCheck,
|
FileCheck,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Bell,
|
Bell,
|
||||||
Camera,
|
Camera,
|
||||||
Activity
|
Activity,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { chemicalsApi, protocolsApi } from "../lib/api";
|
||||||
|
import type { ChemicalInventory, Protocol } from "../shared/types";
|
||||||
|
|
||||||
type Tab = "dashboard" | "inventory" | "protocol";
|
type Tab = "dashboard" | "inventory" | "protocol";
|
||||||
|
|
||||||
@@ -21,56 +23,124 @@ interface DashboardProps {
|
|||||||
setActiveTab: (tab: Tab) => void;
|
setActiveTab: (tab: Tab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function daysUntil(dateStr: string): number {
|
||||||
|
const expDate = new Date(dateStr);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return Math.ceil((expDate.getTime() - today.getTime()) / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(dateStr: string): string {
|
||||||
|
const diffMs = Date.now() - new Date(dateStr).getTime();
|
||||||
|
const mins = Math.floor(diffMs / 60000);
|
||||||
|
const hours = Math.floor(mins / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (mins < 60) return `${mins}m ago`;
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
return `${days}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
export function Dashboard({ setActiveTab }: DashboardProps) {
|
export function Dashboard({ setActiveTab }: DashboardProps) {
|
||||||
|
const [chemicals, setChemicals] = useState<ChemicalInventory[]>([]);
|
||||||
|
const [protocols, setProtocols] = useState<Protocol[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([chemicalsApi.list(), protocolsApi.list()])
|
||||||
|
.then(([chems, protos]) => {
|
||||||
|
setChemicals(chems);
|
||||||
|
setProtocols(protos);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center p-8">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Derived data ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const lowStockChems = chemicals
|
||||||
|
.filter(c => c.percentageFull != null && c.percentageFull < 20)
|
||||||
|
.sort((a, b) => (a.percentageFull ?? 0) - (b.percentageFull ?? 0));
|
||||||
|
|
||||||
|
const expirationChems = chemicals
|
||||||
|
.filter(c => c.expirationDate && daysUntil(c.expirationDate) <= 30)
|
||||||
|
.sort((a, b) => daysUntil(a.expirationDate!) - daysUntil(b.expirationDate!));
|
||||||
|
|
||||||
|
const analyzedProtocols = protocols.filter(p => p.analysis_results?.length);
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: "Chemicals Tracked", value: "252", icon: Package, color: "text-[#5a9584]" },
|
{ label: "Chemicals Tracked", value: chemicals.length, icon: Package, color: "text-[#5a9584]" },
|
||||||
{ label: "Low Stock (<20%)", value: "2", icon: AlertTriangle, color: "text-amber-600" },
|
{ label: "Low Stock (<20%)", value: lowStockChems.length, icon: AlertTriangle, color: "text-amber-600" },
|
||||||
{ label: "Expiring Soon", value: "3", icon: Clock, color: "text-red-600" },
|
{ label: "Expiring ≤30 Days", value: expirationChems.length, icon: Clock, color: "text-red-600" },
|
||||||
{ label: "Protocols Reviewed", value: "12", icon: FileCheck, color: "text-[#2d5a4a]" },
|
{ label: "Protocols Reviewed", value: analyzedProtocols.length, icon: FileCheck, color: "text-[#2d5a4a]" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const lowStockAlerts = [
|
// Recent activity: merge newest chemicals + protocols, take top 4
|
||||||
{ chemical: "Sodium Hydroxide", location: "Cabinet B-1", percentFull: 15, lab: "Lab 305" },
|
type ActivityItem = { label: string; time: string; ts: number; type: string };
|
||||||
{ chemical: "Ethanol", location: "Cabinet A-5", percentFull: 18, lab: "Lab 201" },
|
const activity: ActivityItem[] = [
|
||||||
];
|
...chemicals
|
||||||
|
.filter(c => c.created_at)
|
||||||
|
.map(c => ({
|
||||||
|
label: `Added ${c.chemicalName} to inventory`,
|
||||||
|
time: timeAgo(c.created_at!),
|
||||||
|
ts: new Date(c.created_at!).getTime(),
|
||||||
|
type: "inventory",
|
||||||
|
})),
|
||||||
|
...protocols.map(p => ({
|
||||||
|
label: `${p.title} protocol ${p.analysis_results?.length ? "reviewed" : "uploaded"}`,
|
||||||
|
time: timeAgo(p.created_at),
|
||||||
|
ts: new Date(p.created_at).getTime(),
|
||||||
|
type: "protocol",
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
.sort((a, b) => b.ts - a.ts)
|
||||||
|
.slice(0, 4);
|
||||||
|
|
||||||
const expirationAlerts = [
|
// Safety alerts derived from real data
|
||||||
{ chemical: "Hydrochloric Acid", location: "Acid Cabinet", daysLeft: -2, status: "expired", lab: "Lab 201" },
|
const expiredChems = chemicals.filter(c => c.expirationDate && daysUntil(c.expirationDate) < 0);
|
||||||
{ chemical: "Benzene", location: "Flammables Cabinet", daysLeft: 15, status: "expiring-soon", lab: "Lab 305" },
|
const soonChems = chemicals.filter(
|
||||||
{ chemical: "Acetone", location: "Cabinet A-3", daysLeft: 28, status: "expiring-soon", lab: "Lab 201" },
|
c => c.expirationDate && daysUntil(c.expirationDate) >= 0 && daysUntil(c.expirationDate) <= 30
|
||||||
];
|
);
|
||||||
|
|
||||||
const recentActivity = [
|
type AlertItem = { message: string; severity: "critical" | "warning" | "info" };
|
||||||
{ action: "Scanned and added Methanol via photo", time: "30 mins ago", type: "inventory" },
|
const safetyAlerts: AlertItem[] = [
|
||||||
{ action: "Protocol safety review completed", time: "2 hours ago", type: "protocol" },
|
...expiredChems.map(c => ({
|
||||||
{ action: "Low stock alert: Sodium Hydroxide", time: "4 hours ago", type: "alert" },
|
message: `${c.chemicalName} expired ${Math.abs(daysUntil(c.expirationDate!))}d ago — verify disposal`,
|
||||||
{ action: "Expiration reminder sent for 3 chemicals", time: "1 day ago", type: "reminder" },
|
severity: "critical" as const,
|
||||||
];
|
})),
|
||||||
|
...lowStockChems.slice(0, 2).map(c => ({
|
||||||
const alerts = [
|
message: `${c.chemicalName} is ${c.percentageFull}% full — consider reordering`,
|
||||||
{ message: "Hydrochloric Acid expired 2 days ago - requires disposal", severity: "critical" },
|
severity: "warning" as const,
|
||||||
{ message: "Sodium Hydroxide below 20% full - reorder needed", severity: "warning" },
|
})),
|
||||||
{ message: "3 chemicals expiring in next 30 days", severity: "warning" },
|
...(soonChems.length > 0
|
||||||
|
? [{ message: `${soonChems.length} chemical${soonChems.length > 1 ? "s" : ""} expiring in the next 30 days`, severity: "warning" as const }]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-foreground mb-2">Welcome to Labwise</h1>
|
<h1 className="text-foreground mb-2">Welcome to LabWise</h1>
|
||||||
<p className="text-muted-foreground">Your AI-powered lab safety and compliance assistant</p>
|
<p className="text-muted-foreground">Your AI-powered lab safety and compliance assistant</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
{stats.map((stat) => {
|
{stats.map(stat => {
|
||||||
const Icon = stat.icon;
|
const Icon = stat.icon;
|
||||||
return (
|
return (
|
||||||
<Card key={stat.label} className="p-6">
|
<Card key={stat.label} className="p-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground mb-1">{stat.label}</p>
|
<p className="text-muted-foreground mb-1">{stat.label}</p>
|
||||||
<p className={`${stat.color}`}>{stat.value}</p>
|
<p className={`text-2xl font-bold ${stat.color}`}>{stat.value}</p>
|
||||||
</div>
|
</div>
|
||||||
<Icon className={`w-8 h-8 ${stat.color}`} />
|
<Icon className={`w-8 h-8 ${stat.color}`} />
|
||||||
</div>
|
</div>
|
||||||
@@ -91,10 +161,7 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
<p className="text-muted-foreground">Ask questions with sourced answers</p>
|
<p className="text-muted-foreground">Ask questions with sourced answers</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={() => setActiveTab("protocol")} className="w-full bg-primary hover:bg-primary/90">
|
||||||
onClick={() => setActiveTab("protocol")}
|
|
||||||
className="w-full bg-primary hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
Ask Question <ArrowRight className="w-4 h-4 ml-2" />
|
Ask Question <ArrowRight className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -109,11 +176,7 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
<p className="text-muted-foreground">Get AI safety feedback</p>
|
<p className="text-muted-foreground">Get AI safety feedback</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={() => setActiveTab("protocol")} variant="outline" className="w-full">
|
||||||
onClick={() => setActiveTab("protocol")}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Upload Protocol <ArrowRight className="w-4 h-4 ml-2" />
|
Upload Protocol <ArrowRight className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -128,11 +191,7 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
<p className="text-muted-foreground">Photo capture with auto-fill</p>
|
<p className="text-muted-foreground">Photo capture with auto-fill</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={() => setActiveTab("inventory")} variant="outline" className="w-full">
|
||||||
onClick={() => setActiveTab("inventory")}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Scan Label <Camera className="w-4 h-4 ml-2" />
|
Scan Label <Camera className="w-4 h-4 ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -146,31 +205,31 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||||||
<h3 className="text-foreground">Low Stock Alerts</h3>
|
<h3 className="text-foreground">Low Stock Alerts</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="ghost" className="text-primary" onClick={() => setActiveTab("inventory")}>
|
||||||
variant="ghost"
|
|
||||||
className="text-primary"
|
|
||||||
onClick={() => setActiveTab("inventory")}
|
|
||||||
>
|
|
||||||
View All
|
View All
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
{lowStockChems.length === 0 ? (
|
||||||
{lowStockAlerts.map((alert, idx) => (
|
<p className="text-sm text-muted-foreground py-4 text-center">No low stock items.</p>
|
||||||
<div
|
) : (
|
||||||
key={idx}
|
<div className="space-y-3">
|
||||||
className="flex items-center justify-between p-3 rounded-lg border border-amber-200 bg-amber-50 hover:shadow-sm transition-shadow cursor-pointer"
|
{lowStockChems.slice(0, 3).map(c => (
|
||||||
onClick={() => setActiveTab("inventory")}
|
<div
|
||||||
>
|
key={c.id}
|
||||||
<div className="flex-1">
|
className="flex items-center justify-between p-3 rounded-lg border border-amber-200 bg-amber-50 hover:shadow-sm transition-shadow cursor-pointer"
|
||||||
<p className="text-foreground">{alert.chemical}</p>
|
onClick={() => setActiveTab("inventory")}
|
||||||
<p className="text-muted-foreground text-sm">{alert.lab} • {alert.location}</p>
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-foreground truncate">{c.chemicalName}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">{c.lab} · {c.storageLocation}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="ml-2 shrink-0 bg-amber-100 text-amber-700 border-amber-200">
|
||||||
|
{c.percentageFull}% full
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Badge className="bg-amber-100 text-amber-700 border-amber-200">
|
))}
|
||||||
{alert.percentFull}% full
|
</div>
|
||||||
</Badge>
|
)}
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Expiration Alerts */}
|
{/* Expiration Alerts */}
|
||||||
@@ -180,45 +239,39 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
<Bell className="w-5 h-5 text-primary" />
|
<Bell className="w-5 h-5 text-primary" />
|
||||||
<h3 className="text-foreground">Expiration Alerts</h3>
|
<h3 className="text-foreground">Expiration Alerts</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="ghost" className="text-primary" onClick={() => setActiveTab("inventory")}>
|
||||||
variant="ghost"
|
|
||||||
className="text-primary"
|
|
||||||
onClick={() => setActiveTab("inventory")}
|
|
||||||
>
|
|
||||||
View All
|
View All
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
{expirationChems.length === 0 ? (
|
||||||
{expirationAlerts.map((alert, idx) => (
|
<p className="text-sm text-muted-foreground py-4 text-center">No chemicals expiring within 30 days.</p>
|
||||||
<div
|
) : (
|
||||||
key={idx}
|
<div className="space-y-3">
|
||||||
className="flex items-center justify-between p-4 rounded-lg border border-border hover:shadow-sm transition-shadow cursor-pointer"
|
{expirationChems.slice(0, 4).map(c => {
|
||||||
onClick={() => setActiveTab("inventory")}
|
const days = daysUntil(c.expirationDate!);
|
||||||
>
|
const expired = days < 0;
|
||||||
<div className="flex items-center gap-3 flex-1">
|
return (
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
<div
|
||||||
alert.status === 'expired'
|
key={c.id}
|
||||||
? 'bg-red-500'
|
className="flex items-center justify-between p-4 rounded-lg border border-border hover:shadow-sm transition-shadow cursor-pointer"
|
||||||
: 'bg-amber-500'
|
onClick={() => setActiveTab("inventory")}
|
||||||
}`} />
|
>
|
||||||
<div className="flex-1">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<p className="text-foreground">{alert.chemical}</p>
|
<div className={`w-2 h-2 rounded-full shrink-0 ${expired ? "bg-red-500" : "bg-amber-500"}`} />
|
||||||
<p className="text-muted-foreground text-sm">{alert.lab} • {alert.location}</p>
|
<div className="min-w-0">
|
||||||
|
<p className="text-foreground truncate">{c.chemicalName}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">{c.lab} · {c.storageLocation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className={`ml-2 shrink-0 border ${expired ? "bg-red-100 text-red-700 border-red-200" : "bg-amber-100 text-amber-700 border-amber-200"}`}>
|
||||||
|
<Clock className="w-3 h-3 mr-1" />
|
||||||
|
{expired ? `Expired ${Math.abs(days)}d ago` : `${days}d left`}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<Badge className={`${
|
})}
|
||||||
alert.status === 'expired'
|
</div>
|
||||||
? 'bg-red-100 text-red-700 border-red-200'
|
)}
|
||||||
: 'bg-amber-100 text-amber-700 border-amber-200'
|
|
||||||
} border`}>
|
|
||||||
<Clock className="w-3 h-3 mr-1" />
|
|
||||||
{alert.status === 'expired'
|
|
||||||
? `Expired ${Math.abs(alert.daysLeft)}d ago`
|
|
||||||
: `${alert.daysLeft}d left`}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -226,50 +279,55 @@ export function Dashboard({ setActiveTab }: DashboardProps) {
|
|||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-foreground mb-4">Recent Activity</h3>
|
<h3 className="text-foreground mb-4">Recent Activity</h3>
|
||||||
<div className="space-y-4">
|
{activity.length === 0 ? (
|
||||||
{recentActivity.map((activity, idx) => (
|
<p className="text-sm text-muted-foreground py-4 text-center">No activity yet. Add chemicals or upload a protocol to get started.</p>
|
||||||
<div key={idx} className="flex items-start gap-3">
|
) : (
|
||||||
<Clock className="w-5 h-5 text-muted-foreground mt-0.5" />
|
<div className="space-y-4">
|
||||||
<div className="flex-1">
|
{activity.map((item, idx) => (
|
||||||
<p className="text-foreground">{activity.action}</p>
|
<div key={idx} className="flex items-start gap-3">
|
||||||
<p className="text-muted-foreground">{activity.time}</p>
|
<Activity className="w-5 h-5 text-muted-foreground mt-0.5 shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-foreground truncate">{item.label}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">{item.time}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Safety Alerts */}
|
{/* Safety Alerts */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h3 className="text-foreground mb-4">Safety Alerts</h3>
|
<h3 className="text-foreground mb-4">Safety Alerts</h3>
|
||||||
<div className="space-y-4">
|
{safetyAlerts.length === 0 ? (
|
||||||
{alerts.map((alert, idx) => (
|
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent border border-border">
|
||||||
<div
|
<AlertTriangle className="w-5 h-5 mt-0.5 text-primary" />
|
||||||
key={idx}
|
<p className="text-foreground">All clear — no active safety alerts.</p>
|
||||||
className={`flex items-start gap-3 p-3 rounded-lg ${
|
</div>
|
||||||
alert.severity === "critical"
|
) : (
|
||||||
? "bg-red-50 border border-red-200"
|
<div className="space-y-4">
|
||||||
: alert.severity === "warning"
|
{safetyAlerts.map((alert, idx) => (
|
||||||
? "bg-amber-50 border border-amber-200"
|
<div
|
||||||
: "bg-accent border border-border"
|
key={idx}
|
||||||
}`}
|
className={`flex items-start gap-3 p-3 rounded-lg ${
|
||||||
>
|
|
||||||
<AlertTriangle
|
|
||||||
className={`w-5 h-5 mt-0.5 ${
|
|
||||||
alert.severity === "critical"
|
alert.severity === "critical"
|
||||||
? "text-red-600"
|
? "bg-red-50 border border-red-200"
|
||||||
: alert.severity === "warning"
|
: "bg-amber-50 border border-amber-200"
|
||||||
? "text-amber-600"
|
}`}
|
||||||
: "text-primary"
|
>
|
||||||
}`}
|
<AlertTriangle
|
||||||
/>
|
className={`w-5 h-5 mt-0.5 shrink-0 ${
|
||||||
<p className="text-foreground flex-1">{alert.message}</p>
|
alert.severity === "critical" ? "text-red-600" : "text-amber-600"
|
||||||
</div>
|
}`}
|
||||||
))}
|
/>
|
||||||
</div>
|
<p className="text-foreground flex-1">{alert.message}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,11 +45,11 @@ router.post('/', async (req, res) => {
|
|||||||
req.user!.id, b.piFirstName, b.physicalState, b.chemicalName, b.bldgCode, b.lab,
|
req.user!.id, b.piFirstName, b.physicalState, b.chemicalName, b.bldgCode, b.lab,
|
||||||
b.storageLocation, b.storageDevice, b.numberOfContainers, b.amountPerContainer,
|
b.storageLocation, b.storageDevice, b.numberOfContainers, b.amountPerContainer,
|
||||||
b.unitOfMeasure, b.casNumber,
|
b.unitOfMeasure, b.casNumber,
|
||||||
b.chemicalFormula ?? null, b.molecularWeight ?? null, b.vendor ?? null,
|
b.chemicalFormula || null, b.molecularWeight || null, b.vendor || null,
|
||||||
b.catalogNumber ?? null, b.lotNumber ?? null,
|
b.catalogNumber || null, b.lotNumber || null,
|
||||||
b.expirationDate ?? null, b.concentration ?? null,
|
b.expirationDate || null, b.concentration || null,
|
||||||
b.percentageFull ?? null, b.needsManualEntry ?? null,
|
b.percentageFull ?? null, b.needsManualEntry ?? null,
|
||||||
b.scannedImage ?? null, b.comments ?? null, b.barcode ?? null, b.contact ?? null,
|
b.scannedImage || null, b.comments || null, b.barcode || null, b.contact || null,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
res.status(201).json(snakeToCamel(result.rows[0]));
|
res.status(201).json(snakeToCamel(result.rows[0]));
|
||||||
@@ -62,12 +62,13 @@ router.post('/', async (req, res) => {
|
|||||||
// PATCH /api/chemicals/:id
|
// PATCH /api/chemicals/:id
|
||||||
router.patch('/:id', async (req, res) => {
|
router.patch('/:id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const fields = Object.keys(req.body);
|
const skip = new Set(['id', 'user_id', 'created_at', 'updated_at']);
|
||||||
|
const fields = Object.keys(req.body).filter(k => !skip.has(k) && !skip.has(camelToSnake(k)));
|
||||||
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||||
|
|
||||||
const snakeFields = fields.map(camelToSnake);
|
const snakeFields = fields.map(camelToSnake);
|
||||||
const setClauses = snakeFields.map((f, i) => `${f} = $${i + 3}`).join(', ');
|
const setClauses = snakeFields.map((f, i) => `${f} = $${i + 3}`).join(', ');
|
||||||
const values = fields.map(f => req.body[f]);
|
const values = fields.map(f => req.body[f] || null);
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE chemicals SET ${setClauses}, updated_at = NOW()
|
`UPDATE chemicals SET ${setClauses}, updated_at = NOW()
|
||||||
|
|||||||
Reference in New Issue
Block a user