import { useEffect, useState } from "react"; import { Card } from "./ui/card"; import { Button } from "./ui/button"; import { Badge } from "./ui/badge"; import { AlertTriangle, Clock, Package, FileCheck, MessageSquare, ArrowRight, Bell, Camera, Activity, Loader2, } from "lucide-react"; import { chemicalsApi, protocolsApi } from "../lib/api"; import type { ChemicalInventory, Protocol } from "../shared/types"; type Tab = "dashboard" | "inventory" | "protocol"; interface DashboardProps { 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) { const [chemicals, setChemicals] = useState([]); const [protocols, setProtocols] = useState([]); 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 (
); } // ── 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 = [ { label: "Chemicals Tracked", value: chemicals.length, icon: Package, color: "text-[#5a9584]" }, { label: "Low Stock (<20%)", value: lowStockChems.length, icon: AlertTriangle, color: "text-amber-600" }, { label: "Expiring ≤30 Days", value: expirationChems.length, icon: Clock, color: "text-red-600" }, { label: "Protocols Reviewed", value: analyzedProtocols.length, icon: FileCheck, color: "text-[#2d5a4a]" }, ]; // Recent activity: merge newest chemicals + protocols, take top 4 type ActivityItem = { label: string; time: string; ts: number; type: string }; 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); // Safety alerts derived from real data const expiredChems = chemicals.filter(c => c.expirationDate && daysUntil(c.expirationDate) < 0); const soonChems = chemicals.filter( c => c.expirationDate && daysUntil(c.expirationDate) >= 0 && daysUntil(c.expirationDate) <= 30 ); type AlertItem = { message: string; severity: "critical" | "warning" | "info" }; const safetyAlerts: AlertItem[] = [ ...expiredChems.map(c => ({ message: `${c.chemicalName} expired ${Math.abs(daysUntil(c.expirationDate!))}d ago — verify disposal`, severity: "critical" as const, })), ...lowStockChems.slice(0, 2).map(c => ({ message: `${c.chemicalName} is ${c.percentageFull}% full — consider reordering`, severity: "warning" as const, })), ...(soonChems.length > 0 ? [{ message: `${soonChems.length} chemical${soonChems.length > 1 ? "s" : ""} expiring in the next 30 days`, severity: "warning" as const }] : []), ]; return (

Welcome to LabWise

Your AI-powered lab safety and compliance assistant

{/* Stats Grid */}
{stats.map(stat => { const Icon = stat.icon; return (

{stat.label}

{stat.value}

); })}
{/* Quick Actions */}

AI Safety Assistant

Ask questions with sourced answers

Check Protocol

Get AI safety feedback

Scan Chemical

Photo capture with auto-fill

{/* Low Stock Alerts */}

Low Stock Alerts

{lowStockChems.length === 0 ? (

No low stock items.

) : (
{lowStockChems.slice(0, 3).map(c => (
setActiveTab("inventory")} >

{c.chemicalName}

{c.lab} · {c.storageLocation}

{c.percentageFull}% full
))}
)}
{/* Expiration Alerts */}

Expiration Alerts

{expirationChems.length === 0 ? (

No chemicals expiring within 30 days.

) : (
{expirationChems.slice(0, 4).map(c => { const days = daysUntil(c.expirationDate!); const expired = days < 0; return (
setActiveTab("inventory")} >

{c.chemicalName}

{c.lab} · {c.storageLocation}

{expired ? `Expired ${Math.abs(days)}d ago` : `${days}d left`}
); })}
)}
{/* Recent Activity */}

Recent Activity

{activity.length === 0 ? (

No activity yet. Add chemicals or upload a protocol to get started.

) : (
{activity.map((item, idx) => (

{item.label}

{item.time}

))}
)}
{/* Safety Alerts */}

Safety Alerts

{safetyAlerts.length === 0 ? (

All clear — no active safety alerts.

) : (
{safetyAlerts.map((alert, idx) => (

{alert.message}

))}
)}
); }