353 lines
18 KiB
TypeScript
353 lines
18 KiB
TypeScript
import { useState } from 'react';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
Clock,
|
|
ArrowRight,
|
|
ChevronRight,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Minus,
|
|
Thermometer,
|
|
Droplets,
|
|
Leaf,
|
|
Activity,
|
|
MoreHorizontal
|
|
} from 'lucide-react';
|
|
import { Card } from '../components/ui/card';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { cn } from '../lib/utils';
|
|
|
|
// Types
|
|
type RoomStatus = 'OK' | 'WARNING' | 'CRITICAL';
|
|
type RoomPhase = 'VEG' | 'FLOWER' | 'DRY' | 'CURE';
|
|
type Trend = 'up' | 'down' | 'stable';
|
|
|
|
interface Room {
|
|
id: string;
|
|
name: string;
|
|
phase: RoomPhase;
|
|
status: RoomStatus;
|
|
strains: string[];
|
|
metrics: { temp: number; humidity: number; vpd: number; co2: number };
|
|
trend: Trend;
|
|
tasks: { due: number; completed: number };
|
|
issue?: string;
|
|
}
|
|
|
|
// Mock Data - separated by status for different display treatments
|
|
const MOCK_ROOMS: Room[] = [
|
|
{ id: '1', name: 'Flower Room A', phase: 'FLOWER', status: 'OK', strains: ['OG Kush', 'Gelato #41'], metrics: { temp: 78.4, humidity: 52, vpd: 1.25, co2: 1200 }, trend: 'stable', tasks: { due: 4, completed: 12 } },
|
|
{ id: '2', name: 'Flower Room B', phase: 'FLOWER', status: 'WARNING', strains: ['Blue Dream'], metrics: { temp: 82.1, humidity: 68, vpd: 1.10, co2: 1150 }, trend: 'up', tasks: { due: 8, completed: 5 }, issue: 'Humidity climbing' },
|
|
{ id: '3', name: 'Veg Room 1', phase: 'VEG', status: 'OK', strains: ['Clones - Batch 402'], metrics: { temp: 76.2, humidity: 65, vpd: 0.85, co2: 800 }, trend: 'stable', tasks: { due: 2, completed: 20 } },
|
|
{ id: '4', name: 'Veg Room 2', phase: 'VEG', status: 'OK', strains: ['Mothers'], metrics: { temp: 75.8, humidity: 62, vpd: 0.90, co2: 800 }, trend: 'stable', tasks: { due: 3, completed: 15 } },
|
|
{ id: '5', name: 'Drying Room', phase: 'DRY', status: 'OK', strains: ['Harvest 12/15'], metrics: { temp: 62.0, humidity: 60, vpd: 0.75, co2: 400 }, trend: 'down', tasks: { due: 1, completed: 30 } },
|
|
{ id: '6', name: 'Cure Vault', phase: 'CURE', status: 'OK', strains: ['Bulk - Wedding Cake'], metrics: { temp: 65.4, humidity: 58, vpd: 0.80, co2: 400 }, trend: 'stable', tasks: { due: 0, completed: 45 } },
|
|
{ id: '7', name: 'Flower Room C', phase: 'FLOWER', status: 'CRITICAL', strains: ['Runtz'], metrics: { temp: 88.5, humidity: 72, vpd: 0.95, co2: 1250 }, trend: 'up', tasks: { due: 12, completed: 2 }, issue: 'Temperature exceeded threshold' },
|
|
{ id: '8', name: 'Flower Room D', phase: 'FLOWER', status: 'OK', strains: ['Sour Diesel'], metrics: { temp: 77.9, humidity: 50, vpd: 1.30, co2: 1200 }, trend: 'stable', tasks: { due: 5, completed: 10 } },
|
|
];
|
|
|
|
export default function DashboardPage() {
|
|
const { user } = useAuth();
|
|
const [expandedRoom, setExpandedRoom] = useState<string | null>(null);
|
|
|
|
// Separate rooms by urgency
|
|
const criticalRooms = MOCK_ROOMS.filter(r => r.status === 'CRITICAL');
|
|
const warningRooms = MOCK_ROOMS.filter(r => r.status === 'WARNING');
|
|
const healthyRooms = MOCK_ROOMS.filter(r => r.status === 'OK');
|
|
|
|
const healthyCount = healthyRooms.length;
|
|
const totalRooms = MOCK_ROOMS.length;
|
|
|
|
return (
|
|
<div className="space-y-6 pb-12 max-w-7xl mx-auto">
|
|
{/* Compact Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
|
Facility Overview
|
|
</h1>
|
|
<p className="text-sm text-[var(--color-text-tertiary)] mt-1">
|
|
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick Status Summary */}
|
|
<div className="flex items-center gap-6">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-[var(--color-primary)]" />
|
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">
|
|
{healthyCount}/{totalRooms} systems nominal
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attention Required Section - Only shows if issues exist */}
|
|
{(criticalRooms.length > 0 || warningRooms.length > 0) && (
|
|
<section className="space-y-4">
|
|
<h2 className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-tertiary)] flex items-center gap-2">
|
|
<AlertCircle size={14} className="text-[var(--color-error)]" />
|
|
Attention Required
|
|
<span className="px-1.5 py-0.5 rounded-full bg-[var(--color-error)] text-[var(--color-text-inverse)] text-[10px] font-bold">
|
|
{criticalRooms.length + warningRooms.length}
|
|
</span>
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Critical items get largest cards */}
|
|
{criticalRooms.map(room => (
|
|
<AttentionCard key={room.id} room={room} severity="critical" />
|
|
))}
|
|
{/* Warnings are slightly smaller */}
|
|
{warningRooms.map(room => (
|
|
<AttentionCard key={room.id} room={room} severity="warning" />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* All Systems Status - Compact Table */}
|
|
<section className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-tertiary)] flex items-center gap-2">
|
|
<Activity size={14} />
|
|
All Zones
|
|
</h2>
|
|
<button className="text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-primary)] transition-colors">
|
|
View All Details
|
|
</button>
|
|
</div>
|
|
|
|
<Card className="bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] overflow-hidden">
|
|
{/* Table Header */}
|
|
<div className="grid grid-cols-12 gap-4 px-4 py-3 bg-[var(--color-bg-tertiary)]/50 border-b border-[var(--color-border-subtle)] text-[10px] font-bold uppercase tracking-wider text-[var(--color-text-tertiary)]">
|
|
<div className="col-span-3">Zone</div>
|
|
<div className="col-span-2">Phase</div>
|
|
<div className="col-span-2 text-center">Status</div>
|
|
<div className="col-span-2 text-center">Temp</div>
|
|
<div className="col-span-2 text-center">RH</div>
|
|
<div className="col-span-1"></div>
|
|
</div>
|
|
|
|
{/* Table Rows */}
|
|
<div className="divide-y divide-[var(--color-border-subtle)]">
|
|
{MOCK_ROOMS.map(room => (
|
|
<RoomRow
|
|
key={room.id}
|
|
room={room}
|
|
isExpanded={expandedRoom === room.id}
|
|
onToggle={() => setExpandedRoom(expandedRoom === room.id ? null : room.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</section>
|
|
|
|
{/* Bottom Stats - Very Minimal */}
|
|
<section className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<MiniStat label="Active Batches" value="24" trend="stable" />
|
|
<MiniStat label="Tasks Due Today" value="18" trend="down" />
|
|
<MiniStat label="Avg Temperature" value="76.4°F" trend="stable" />
|
|
<MiniStat label="Avg Humidity" value="58%" trend="stable" />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Attention Card - Only for issues that need action
|
|
function AttentionCard({ room, severity }: { room: Room, severity: 'critical' | 'warning' }) {
|
|
const isCritical = severity === 'critical';
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<Card className={cn(
|
|
"p-4 cursor-pointer transition-all hover:shadow-lg",
|
|
isCritical
|
|
? "bg-[var(--color-error)]/5 border-[var(--color-error)]/30 hover:border-[var(--color-error)]/50"
|
|
: "bg-[var(--color-warning)]/5 border-[var(--color-warning)]/30 hover:border-[var(--color-warning)]/50"
|
|
)}>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<AlertCircle size={16} className={isCritical ? "text-[var(--color-error)]" : "text-[var(--color-warning)]"} />
|
|
<span className="font-bold text-[var(--color-text-primary)]">{room.name}</span>
|
|
<span className={cn(
|
|
"px-1.5 py-0.5 rounded text-[9px] font-bold uppercase",
|
|
isCritical ? "bg-[var(--color-error)]/20 text-[var(--color-error)]" : "bg-[var(--color-warning)]/20 text-[var(--color-warning)]"
|
|
)}>
|
|
{severity}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-[var(--color-text-secondary)] mb-3">
|
|
{room.issue}
|
|
</p>
|
|
<div className="flex items-center gap-4 text-xs text-[var(--color-text-tertiary)]">
|
|
<span className="flex items-center gap-1">
|
|
<Thermometer size={12} />
|
|
{room.metrics.temp}°F
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Droplets size={12} />
|
|
{room.metrics.humidity}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button className={cn(
|
|
"px-3 py-1.5 rounded-lg text-xs font-bold transition-colors",
|
|
isCritical
|
|
? "bg-[var(--color-error)] text-white hover:bg-[var(--color-error)]/80"
|
|
: "bg-[var(--color-warning)] text-[var(--color-text-inverse)] hover:bg-[var(--color-warning)]/80"
|
|
)}>
|
|
Resolve
|
|
</button>
|
|
</div>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
// Compact Row for All Systems table
|
|
function RoomRow({ room, isExpanded, onToggle }: { room: Room, isExpanded: boolean, onToggle: () => void }) {
|
|
const statusDot: Record<RoomStatus, string> = {
|
|
OK: 'bg-[var(--color-primary)]',
|
|
WARNING: 'bg-[var(--color-warning)]',
|
|
CRITICAL: 'bg-[var(--color-error)]'
|
|
};
|
|
|
|
const TrendIcon = room.trend === 'up' ? TrendingUp : room.trend === 'down' ? TrendingDown : Minus;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={cn(
|
|
"grid grid-cols-12 gap-4 px-4 py-3 items-center cursor-pointer transition-colors",
|
|
room.status === 'OK' ? "hover:bg-[var(--color-bg-tertiary)]/30" : "",
|
|
room.status === 'WARNING' && "bg-[var(--color-warning)]/5",
|
|
room.status === 'CRITICAL' && "bg-[var(--color-error)]/5"
|
|
)}
|
|
onClick={onToggle}
|
|
>
|
|
{/* Zone Name */}
|
|
<div className="col-span-3 flex items-center gap-2">
|
|
<div className={cn("w-2 h-2 rounded-full", statusDot[room.status])} />
|
|
<span className="font-medium text-sm text-[var(--color-text-primary)] truncate">{room.name}</span>
|
|
</div>
|
|
|
|
{/* Phase */}
|
|
<div className="col-span-2">
|
|
<span className={cn(
|
|
"px-2 py-0.5 rounded text-[10px] font-bold uppercase",
|
|
room.phase === 'FLOWER' ? "bg-purple-500/10 text-purple-400" :
|
|
room.phase === 'VEG' ? "bg-[var(--color-primary)]/10 text-[var(--color-primary)]" :
|
|
"bg-[var(--color-bg-tertiary)] text-[var(--color-text-tertiary)]"
|
|
)}>
|
|
{room.phase}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Status - Very subtle for OK */}
|
|
<div className="col-span-2 text-center">
|
|
{room.status === 'OK' ? (
|
|
<span className="text-xs text-[var(--color-text-tertiary)]">Normal</span>
|
|
) : (
|
|
<span className={cn(
|
|
"text-xs font-bold",
|
|
room.status === 'WARNING' ? "text-[var(--color-warning)]" : "text-[var(--color-error)]"
|
|
)}>
|
|
{room.status}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Temp with trend */}
|
|
<div className="col-span-2 text-center flex items-center justify-center gap-1">
|
|
<span className={cn(
|
|
"text-sm",
|
|
room.metrics.temp > 85 ? "text-[var(--color-error)] font-bold" : "text-[var(--color-text-secondary)]"
|
|
)}>
|
|
{room.metrics.temp}°F
|
|
</span>
|
|
<TrendIcon size={12} className="text-[var(--color-text-quaternary)]" />
|
|
</div>
|
|
|
|
{/* Humidity */}
|
|
<div className="col-span-2 text-center">
|
|
<span className={cn(
|
|
"text-sm",
|
|
room.metrics.humidity > 70 ? "text-[var(--color-warning)] font-bold" : "text-[var(--color-text-secondary)]"
|
|
)}>
|
|
{room.metrics.humidity}%
|
|
</span>
|
|
</div>
|
|
|
|
{/* Expand */}
|
|
<div className="col-span-1 text-right">
|
|
<ChevronRight size={16} className={cn(
|
|
"text-[var(--color-text-quaternary)] transition-transform",
|
|
isExpanded && "rotate-90"
|
|
)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded Details */}
|
|
<AnimatePresence>
|
|
{isExpanded && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
className="overflow-hidden bg-[var(--color-bg-tertiary)]/30 border-t border-[var(--color-border-subtle)]"
|
|
>
|
|
<div className="px-4 py-4 grid grid-cols-4 gap-4">
|
|
<MetricTile label="Temperature" value={`${room.metrics.temp}°F`} target="75-80°F" />
|
|
<MetricTile label="Humidity" value={`${room.metrics.humidity}%`} target="55-65%" />
|
|
<MetricTile label="VPD" value={`${room.metrics.vpd} kPa`} target="1.0-1.3" />
|
|
<MetricTile label="CO2" value={`${room.metrics.co2} ppm`} target="1000-1400" />
|
|
</div>
|
|
<div className="px-4 pb-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4 text-xs text-[var(--color-text-tertiary)]">
|
|
<span>Strains: {room.strains.join(', ')}</span>
|
|
<span>Tasks: {room.tasks.due} pending, {room.tasks.completed} done</span>
|
|
</div>
|
|
<button className="text-xs text-[var(--color-primary)] hover:underline font-medium flex items-center gap-1">
|
|
View Room Details
|
|
<ArrowRight size={12} />
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Minimal Stat Tile
|
|
function MiniStat({ label, value, trend }: { label: string, value: string, trend: 'up' | 'down' | 'stable' }) {
|
|
const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;
|
|
|
|
return (
|
|
<div className="stat-card flex items-center justify-between">
|
|
<div>
|
|
<p className="text-[10px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">{label}</p>
|
|
<p className="text-lg font-bold text-[var(--color-text-primary)]">{value}</p>
|
|
</div>
|
|
<TrendIcon size={16} className="text-[var(--color-text-quaternary)]" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Detail tile for expanded view
|
|
function MetricTile({ label, value, target }: { label: string, value: string, target: string }) {
|
|
return (
|
|
<div className="text-center">
|
|
<p className="text-[10px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wide mb-1">{label}</p>
|
|
<p className="text-lg font-bold text-[var(--color-text-primary)]">{value}</p>
|
|
<p className="text-[10px] text-[var(--color-text-quaternary)]">Target: {target}</p>
|
|
</div>
|
|
);
|
|
}
|