275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
|
|
import { Card } from "./ui/card";
|
||
|
|
import { Button } from "./ui/button";
|
||
|
|
import { Badge } from "./ui/badge";
|
||
|
|
import {
|
||
|
|
AlertTriangle,
|
||
|
|
CheckCircle2,
|
||
|
|
Clock,
|
||
|
|
TrendingUp,
|
||
|
|
Package,
|
||
|
|
FileCheck,
|
||
|
|
MessageSquare,
|
||
|
|
ArrowRight,
|
||
|
|
Bell,
|
||
|
|
Camera,
|
||
|
|
Activity
|
||
|
|
} from "lucide-react";
|
||
|
|
|
||
|
|
type Tab = "dashboard" | "inventory" | "protocol";
|
||
|
|
|
||
|
|
interface DashboardProps {
|
||
|
|
setActiveTab: (tab: Tab) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Dashboard({ setActiveTab }: DashboardProps) {
|
||
|
|
const stats = [
|
||
|
|
{ label: "Chemicals Tracked", value: "252", icon: Package, color: "text-[#5a9584]" },
|
||
|
|
{ label: "Low Stock (<20%)", value: "2", icon: AlertTriangle, color: "text-amber-600" },
|
||
|
|
{ label: "Expiring Soon", value: "3", icon: Clock, color: "text-red-600" },
|
||
|
|
{ label: "Protocols Reviewed", value: "12", icon: FileCheck, color: "text-[#2d5a4a]" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const lowStockAlerts = [
|
||
|
|
{ chemical: "Sodium Hydroxide", location: "Cabinet B-1", percentFull: 15, lab: "Lab 305" },
|
||
|
|
{ chemical: "Ethanol", location: "Cabinet A-5", percentFull: 18, lab: "Lab 201" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const expirationAlerts = [
|
||
|
|
{ chemical: "Hydrochloric Acid", location: "Acid Cabinet", daysLeft: -2, status: "expired", lab: "Lab 201" },
|
||
|
|
{ chemical: "Benzene", location: "Flammables Cabinet", daysLeft: 15, status: "expiring-soon", lab: "Lab 305" },
|
||
|
|
{ chemical: "Acetone", location: "Cabinet A-3", daysLeft: 28, status: "expiring-soon", lab: "Lab 201" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const recentActivity = [
|
||
|
|
{ action: "Scanned and added Methanol via photo", time: "30 mins ago", type: "inventory" },
|
||
|
|
{ action: "Protocol safety review completed", time: "2 hours ago", type: "protocol" },
|
||
|
|
{ action: "Low stock alert: Sodium Hydroxide", time: "4 hours ago", type: "alert" },
|
||
|
|
{ action: "Expiration reminder sent for 3 chemicals", time: "1 day ago", type: "reminder" },
|
||
|
|
];
|
||
|
|
|
||
|
|
const alerts = [
|
||
|
|
{ message: "Hydrochloric Acid expired 2 days ago - requires disposal", severity: "critical" },
|
||
|
|
{ message: "Sodium Hydroxide below 20% full - reorder needed", severity: "warning" },
|
||
|
|
{ message: "3 chemicals expiring in next 30 days", severity: "warning" },
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-8">
|
||
|
|
<div className="max-w-7xl mx-auto">
|
||
|
|
<div className="mb-8">
|
||
|
|
<h1 className="text-foreground mb-2">Welcome to Labwise</h1>
|
||
|
|
<p className="text-muted-foreground">Your AI-powered lab safety and compliance assistant</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats Grid */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||
|
|
{stats.map((stat) => {
|
||
|
|
const Icon = stat.icon;
|
||
|
|
return (
|
||
|
|
<Card key={stat.label} className="p-6">
|
||
|
|
<div className="flex items-start justify-between">
|
||
|
|
<div>
|
||
|
|
<p className="text-muted-foreground mb-1">{stat.label}</p>
|
||
|
|
<p className={`${stat.color}`}>{stat.value}</p>
|
||
|
|
</div>
|
||
|
|
<Icon className={`w-8 h-8 ${stat.color}`} />
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Quick Actions */}
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||
|
|
<Card className="p-6">
|
||
|
|
<div className="flex items-start gap-4 mb-4">
|
||
|
|
<div className="p-3 bg-accent rounded-lg">
|
||
|
|
<MessageSquare className="w-6 h-6 text-primary" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 className="text-foreground mb-1">AI Safety Assistant</h3>
|
||
|
|
<p className="text-muted-foreground">Ask questions with sourced answers</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
onClick={() => setActiveTab("protocol")}
|
||
|
|
className="w-full bg-primary hover:bg-primary/90"
|
||
|
|
>
|
||
|
|
Ask Question <ArrowRight className="w-4 h-4 ml-2" />
|
||
|
|
</Button>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card className="p-6">
|
||
|
|
<div className="flex items-start gap-4 mb-4">
|
||
|
|
<div className="p-3 bg-accent rounded-lg">
|
||
|
|
<FileCheck className="w-6 h-6 text-primary" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 className="text-foreground mb-1">Check Protocol</h3>
|
||
|
|
<p className="text-muted-foreground">Get AI safety feedback</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
onClick={() => setActiveTab("protocol")}
|
||
|
|
variant="outline"
|
||
|
|
className="w-full"
|
||
|
|
>
|
||
|
|
Upload Protocol <ArrowRight className="w-4 h-4 ml-2" />
|
||
|
|
</Button>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card className="p-6">
|
||
|
|
<div className="flex items-start gap-4 mb-4">
|
||
|
|
<div className="p-3 bg-accent rounded-lg">
|
||
|
|
<Camera className="w-6 h-6 text-primary" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 className="text-foreground mb-1">Scan Chemical</h3>
|
||
|
|
<p className="text-muted-foreground">Photo capture with auto-fill</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
onClick={() => setActiveTab("inventory")}
|
||
|
|
variant="outline"
|
||
|
|
className="w-full"
|
||
|
|
>
|
||
|
|
Scan Label <Camera className="w-4 h-4 ml-2" />
|
||
|
|
</Button>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||
|
|
{/* Low Stock Alerts */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||
|
|
<h3 className="text-foreground">Low Stock Alerts</h3>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
className="text-primary"
|
||
|
|
onClick={() => setActiveTab("inventory")}
|
||
|
|
>
|
||
|
|
View All
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-3">
|
||
|
|
{lowStockAlerts.map((alert, idx) => (
|
||
|
|
<div
|
||
|
|
key={idx}
|
||
|
|
className="flex items-center justify-between p-3 rounded-lg border border-amber-200 bg-amber-50 hover:shadow-sm transition-shadow cursor-pointer"
|
||
|
|
onClick={() => setActiveTab("inventory")}
|
||
|
|
>
|
||
|
|
<div className="flex-1">
|
||
|
|
<p className="text-foreground">{alert.chemical}</p>
|
||
|
|
<p className="text-muted-foreground text-sm">{alert.lab} • {alert.location}</p>
|
||
|
|
</div>
|
||
|
|
<Badge className="bg-amber-100 text-amber-700 border-amber-200">
|
||
|
|
{alert.percentFull}% full
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Expiration Alerts */}
|
||
|
|
<Card className="p-6 lg:col-span-2">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Bell className="w-5 h-5 text-primary" />
|
||
|
|
<h3 className="text-foreground">Expiration Alerts</h3>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
className="text-primary"
|
||
|
|
onClick={() => setActiveTab("inventory")}
|
||
|
|
>
|
||
|
|
View All
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-3">
|
||
|
|
{expirationAlerts.map((alert, idx) => (
|
||
|
|
<div
|
||
|
|
key={idx}
|
||
|
|
className="flex items-center justify-between p-4 rounded-lg border border-border hover:shadow-sm transition-shadow cursor-pointer"
|
||
|
|
onClick={() => setActiveTab("inventory")}
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-3 flex-1">
|
||
|
|
<div className={`w-2 h-2 rounded-full ${
|
||
|
|
alert.status === 'expired'
|
||
|
|
? 'bg-red-500'
|
||
|
|
: 'bg-amber-500'
|
||
|
|
}`} />
|
||
|
|
<div className="flex-1">
|
||
|
|
<p className="text-foreground">{alert.chemical}</p>
|
||
|
|
<p className="text-muted-foreground text-sm">{alert.lab} • {alert.location}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Badge className={`${
|
||
|
|
alert.status === 'expired'
|
||
|
|
? '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>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
|
{/* Recent Activity */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<h3 className="text-foreground mb-4">Recent Activity</h3>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{recentActivity.map((activity, idx) => (
|
||
|
|
<div key={idx} className="flex items-start gap-3">
|
||
|
|
<Clock className="w-5 h-5 text-muted-foreground mt-0.5" />
|
||
|
|
<div className="flex-1">
|
||
|
|
<p className="text-foreground">{activity.action}</p>
|
||
|
|
<p className="text-muted-foreground">{activity.time}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Safety Alerts */}
|
||
|
|
<Card className="p-6">
|
||
|
|
<h3 className="text-foreground mb-4">Safety Alerts</h3>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{alerts.map((alert, idx) => (
|
||
|
|
<div
|
||
|
|
key={idx}
|
||
|
|
className={`flex items-start gap-3 p-3 rounded-lg ${
|
||
|
|
alert.severity === "critical"
|
||
|
|
? "bg-red-50 border border-red-200"
|
||
|
|
: alert.severity === "warning"
|
||
|
|
? "bg-amber-50 border border-amber-200"
|
||
|
|
: "bg-accent border border-border"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<AlertTriangle
|
||
|
|
className={`w-5 h-5 mt-0.5 ${
|
||
|
|
alert.severity === "critical"
|
||
|
|
? "text-red-600"
|
||
|
|
: alert.severity === "warning"
|
||
|
|
? "text-amber-600"
|
||
|
|
: "text-primary"
|
||
|
|
}`}
|
||
|
|
/>
|
||
|
|
<p className="text-foreground flex-1">{alert.message}</p>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|