ca-grow-ops-manager/frontend/src/pages/DashboardPage.tsx
fullsizemalt 15a6b08e0f
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix: Add proper TypeScript types to DashboardPage
2025-12-27 12:06:30 -08:00

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>
);
}