fix: TypeScript build errors - add missing imports and fix type mismatch
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Add cn import to UserMenu.tsx
- Add Plus import to RoomDetailPage.tsx
- Fix MOCK_ROOMS strains readonly type in DashboardPage.tsx
- Refactor RoomsPage.tsx with Control Room aesthetic
This commit is contained in:
fullsizemalt 2025-12-19 20:44:19 -08:00
parent dff54a60ce
commit f7a4b32a2c
4 changed files with 177 additions and 99 deletions

View file

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { LogOut, ChevronDown, User } from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { RoleBadge } from '../ui/RoleBadge';
import { cn } from '../../lib/utils';
/**
* User menu dropdown for desktop sidebar

View file

@ -18,16 +18,24 @@ import { Card } from '../components/ui/card';
import { motion } from 'framer-motion';
// Mock Data for Facility Overview
const MOCK_ROOMS = [
{ 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 }, 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 }, tasks: { due: 8, completed: 5 } },
{ 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 }, 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 }, 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 }, 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 }, 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 }, tasks: { due: 12, completed: 2 } },
{ id: '8', name: 'Flower Room D', phase: 'FLOWER', status: 'OK', strains: ['Sour Diesel'], metrics: { temp: 77.9, humidity: 50, vpd: 1.30, co2: 1200 }, tasks: { due: 5, completed: 10 } },
] as const;
const MOCK_ROOMS: Array<{
id: string;
name: string;
phase: 'VEG' | 'FLOWER' | 'DRY' | 'CURE';
status: 'OK' | 'WARNING' | 'CRITICAL';
strains: string[];
metrics: { temp: number; humidity: number; vpd: number; co2: number };
tasks: { due: number; completed: number };
}> = [
{ 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 }, 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 }, tasks: { due: 8, completed: 5 } },
{ 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 }, 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 }, 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 }, 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 }, 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 }, tasks: { due: 12, completed: 2 } },
{ id: '8', name: 'Flower Room D', phase: 'FLOWER', status: 'OK', strains: ['Sour Diesel'], metrics: { temp: 77.9, humidity: 50, vpd: 1.30, co2: 1200 }, tasks: { due: 5, completed: 10 } },
];
const MOCK_ALERTS = [
{ id: '1', level: 'CRITICAL', source: 'Flower Room C', message: 'Temperature threshold exceeded (>85°F)', time: '5m ago' },
@ -121,7 +129,7 @@ export default function DashboardPage() {
<div key={alert.id} className="p-4 hover:bg-slate-50 dark:hover:bg-slate-900/50 transition-colors group cursor-pointer">
<div className="flex items-start gap-3">
<div className={`mt-1 w-1.5 h-1.5 rounded-full shrink-0 ${alert.level === 'CRITICAL' ? 'bg-rose-500' :
alert.level === 'WARNING' ? 'bg-amber-500' : 'bg-blue-500'
alert.level === 'WARNING' ? 'bg-amber-500' : 'bg-blue-500'
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-0.5">

View file

@ -3,7 +3,7 @@ import { useParams, useNavigate, Link } from 'react-router-dom';
import {
ArrowLeft, Thermometer, Droplets, Wind, Zap, Sun,
Sprout, Calendar, Edit2, Layers, Clock, CheckCircle2,
Activity, History, ClipboardList, Filter, MoreHorizontal
Activity, History, ClipboardList, Filter, MoreHorizontal, Plus
} from 'lucide-react';
import api from '../lib/api';
import { Card } from '../components/ui/card';

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Home, Plus, Thermometer, Droplets, ChevronRight } from 'lucide-react';
import { Home, Plus, Thermometer, Droplets, ChevronRight, Activity, Leaf, Flower, ShieldCheck, ArrowRight } from 'lucide-react';
import api from '../lib/api';
import { usePermissions } from '../hooks/usePermissions';
import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
import { Card } from '../components/ui/card';
import { cn } from '../lib/utils';
export default function RoomsPage() {
const { isManager } = usePermissions();
@ -26,112 +27,179 @@ export default function RoomsPage() {
}
};
// Header background colors per room type
const getHeaderStyle = (type: string) => {
const styles: Record<string, { bg: string; text: string; border: string }> = {
VEG: { bg: 'bg-green-500/10', text: 'text-green-600', border: 'border-green-500/20' },
FLOWER: { bg: 'bg-purple-500/10', text: 'text-purple-600', border: 'border-purple-500/20' },
DRY: { bg: 'bg-amber-500/10', text: 'text-amber-600', border: 'border-amber-500/20' },
CURE: { bg: 'bg-orange-500/10', text: 'text-orange-600', border: 'border-orange-500/20' },
MOTHER: { bg: 'bg-pink-500/10', text: 'text-pink-600', border: 'border-pink-500/20' },
TRIM: { bg: 'bg-slate-500/10', text: 'text-slate-600', border: 'border-slate-500/20' },
CLONE: { bg: 'bg-teal-500/10', text: 'text-teal-600', border: 'border-teal-500/20' },
const getRoomStyle = (type: string) => {
const styles: Record<string, { accent: string; icon: any }> = {
VEG: { accent: 'emerald', icon: Leaf },
FLOWER: { accent: 'purple', icon: Flower },
DRY: { accent: 'amber', icon: Activity },
CURE: { accent: 'orange', icon: Activity },
MOTHER: { accent: 'pink', icon: Leaf },
TRIM: { accent: 'slate', icon: Activity },
CLONE: { accent: 'teal', icon: Leaf },
};
return styles[type] || { bg: 'bg-tertiary', text: 'text-secondary', border: 'border-subtle' };
return styles[type] || { accent: 'slate', icon: Home };
};
return (
<div className="space-y-6 animate-in">
<PageHeader
title="Rooms"
subtitle={isLoading ? 'Loading...' : `${rooms.length} cultivation rooms`}
actions={
isManager && (
<button className="btn btn-primary">
<Plus size={16} />
<span className="hidden md:inline">Add Room</span>
</button>
)
}
/>
const vegCount = rooms.filter(r => r.type === 'VEG').length;
const flowerCount = rooms.filter(r => r.type === 'FLOWER').length;
const totalBatches = rooms.reduce((acc, r) => acc + (r.batches?.length || 0), 0);
return (
<div className="space-y-8 pb-12">
{/* Page Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div className="space-y-1">
<h1 className="text-3xl font-extra-bold tracking-tight text-slate-900 dark:text-white uppercase italic">
Cultivation Zones
</h1>
<div className="flex items-center gap-2 text-slate-500 text-xs font-bold uppercase tracking-widest">
<ShieldCheck size={14} className="text-emerald-500" />
<span>{rooms.length} Active Zones Environmental Controls Active</span>
</div>
</div>
{isManager && (
<button className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-lg font-bold text-sm shadow-lg shadow-emerald-500/20 transition-all uppercase tracking-widest">
<Plus size={18} />
Add Zone
</button>
)}
</div>
{/* KPI Strip */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-4 dark:bg-[#13171F] border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest">Total Zones</p>
<p className="text-xl font-black italic tracking-tighter text-slate-900 dark:text-white">{rooms.length}</p>
</div>
<div className="p-2.5 rounded-lg border bg-blue-500/5 text-blue-500 border-blue-500/10">
<Home size={16} />
</div>
</Card>
<Card className="p-4 dark:bg-[#13171F] border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest">Veg Rooms</p>
<p className="text-xl font-black italic tracking-tighter text-slate-900 dark:text-white">{vegCount}</p>
</div>
<div className="p-2.5 rounded-lg border bg-emerald-500/5 text-emerald-500 border-emerald-500/10">
<Leaf size={16} />
</div>
</Card>
<Card className="p-4 dark:bg-[#13171F] border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest">Flower Rooms</p>
<p className="text-xl font-black italic tracking-tighter text-slate-900 dark:text-white">{flowerCount}</p>
</div>
<div className="p-2.5 rounded-lg border bg-purple-500/5 text-purple-500 border-purple-500/10">
<Flower size={16} />
</div>
</Card>
<Card className="p-4 dark:bg-[#13171F] border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest">Active Batches</p>
<p className="text-xl font-black italic tracking-tighter text-slate-900 dark:text-white">{totalBatches}</p>
</div>
<div className="p-2.5 rounded-lg border bg-amber-500/5 text-amber-500 border-amber-500/10">
<Activity size={16} />
</div>
</Card>
</div>
{/* Room Grid */}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => <CardSkeleton key={i} />)}
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i} className="p-6 dark:bg-[#13171F] border-slate-200 dark:border-slate-800 animate-pulse">
<div className="h-4 w-1/2 bg-slate-200 dark:bg-slate-800 rounded mb-4" />
<div className="h-8 w-1/4 bg-slate-200 dark:bg-slate-800 rounded mb-2" />
<div className="h-3 w-1/3 bg-slate-200 dark:bg-slate-800 rounded" />
</Card>
))}
</div>
) : rooms.length === 0 ? (
<EmptyState
icon={Home}
title="No rooms configured"
description="Set up your first cultivation room to start tracking."
action={
isManager && (
<button className="btn btn-primary">
<Plus size={16} />
Create First Room
</button>
)
}
/>
<div className="py-16 text-center rounded-2xl border-2 border-dashed border-slate-100 dark:border-slate-800">
<Home size={40} className="mx-auto text-slate-300 dark:text-slate-700 mb-4" />
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest mb-4">No Cultivation Zones Configured</p>
{isManager && (
<button className="btn btn-primary mt-4">
<Plus size={16} /> Create First Zone
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{rooms.map(room => {
const style = getHeaderStyle(room.type);
const { accent, icon: Icon } = getRoomStyle(room.type);
const accentClasses = {
emerald: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20',
purple: 'bg-purple-500/10 text-purple-500 border-purple-500/20',
amber: 'bg-amber-500/10 text-amber-500 border-amber-500/20',
orange: 'bg-orange-500/10 text-orange-500 border-orange-500/20',
pink: 'bg-pink-500/10 text-pink-500 border-pink-500/20',
slate: 'bg-slate-500/10 text-slate-500 border-slate-500/20',
teal: 'bg-teal-500/10 text-teal-500 border-teal-500/20',
};
return (
<Link
key={room.id}
to={`/rooms/${room.id}`}
className="card overflow-hidden group hover:shadow-md transition-shadow"
className="group block"
>
{/* Color-coded Header */}
<div className={`px-4 py-3 ${style.bg} ${style.border} border-b flex justify-between items-center`}>
<div>
<h3 className={`font-medium text-sm ${style.text}`}>
{room.name?.replace('[DEMO] ', '')}
</h3>
<span className="text-[10px] text-tertiary">{room.sqft?.toLocaleString()} sqft {room.capacity || '—'} cap</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-xs font-semibold ${style.text}`}>
{room.type}
</span>
<ChevronRight size={14} className="text-tertiary opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
{/* Sensor Data - Prominent */}
<div className="p-4">
<div className="flex items-center justify-around py-3 mb-3">
<div className="text-center">
<div className="flex items-center justify-center gap-1 text-2xl font-semibold text-primary">
<Thermometer size={18} className="text-red-500" />
{room.targetTemp || '—'}
<span className="text-sm text-tertiary font-normal">°F</span>
</div>
<span className="text-[10px] text-tertiary uppercase tracking-wide">Temp</span>
</div>
<div className="w-px h-10 bg-subtle" />
<div className="text-center">
<div className="flex items-center justify-center gap-1 text-2xl font-semibold text-primary">
<Droplets size={18} className="text-blue-500" />
{room.targetHumidity || '—'}
<span className="text-sm text-tertiary font-normal">%</span>
</div>
<span className="text-[10px] text-tertiary uppercase tracking-wide">Humidity</span>
<Card className="p-0 overflow-hidden dark:bg-[#13171F] border-slate-200 dark:border-slate-800 hover:border-emerald-500/50 transition-all">
{/* Header */}
<div className={cn("px-5 py-4 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between", accentClasses[accent as keyof typeof accentClasses])}>
<div className="flex items-center gap-3">
<Icon size={16} />
<span className="text-[10px] font-black uppercase tracking-[0.2em]">{room.type}</span>
</div>
<ArrowRight size={14} className="opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
{/* Batch Count */}
<div className="flex items-center justify-between pt-3 border-t border-subtle">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${room.batches?.length > 0 ? 'bg-success' : 'bg-subtle'}`} />
<span className="text-sm font-medium text-primary">
{room.batches?.length || 0} {room.batches?.length === 1 ? 'batch' : 'batches'}
{/* Body */}
<div className="p-5 space-y-4">
<div>
<h3 className="text-lg font-extra-bold text-slate-900 dark:text-white uppercase italic tracking-tight">
{room.name?.replace('[DEMO] ', '')}
</h3>
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-1">
{room.sqft?.toLocaleString()} sqft Capacity {room.capacity || '—'}
</p>
</div>
{/* Environmental Vitals */}
<div className="grid grid-cols-2 gap-4 py-4 border-y border-slate-100 dark:border-slate-800">
<div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-xl font-black tracking-tighter text-slate-900 dark:text-white">
<Thermometer size={14} className="text-rose-500" />
{room.targetTemp || '—'}°F
</div>
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-widest">Target Temp</span>
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-xl font-black tracking-tighter text-slate-900 dark:text-white">
<Droplets size={14} className="text-blue-500" />
{room.targetHumidity || '—'}%
</div>
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-widest">Target RH</span>
</div>
</div>
{/* Batch Status */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn("w-2 h-2 rounded-full", room.batches?.length > 0 ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-700')} />
<span className="text-xs font-bold text-slate-700 dark:text-slate-300">
{room.batches?.length || 0} Active Batch{room.batches?.length === 1 ? '' : 'es'}
</span>
</div>
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">
Enter
</span>
</div>
<span className="text-xs text-accent opacity-0 group-hover:opacity-100 transition-opacity">View </span>
</div>
</div>
</Card>
</Link>
);
})}
@ -140,3 +208,4 @@ export default function RoomsPage() {
</div>
);
}