fix: TypeScript build errors - add missing imports and fix type mismatch
- 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:
parent
dff54a60ce
commit
f7a4b32a2c
4 changed files with 177 additions and 99 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@ import { Card } from '../components/ui/card';
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
// Mock Data for Facility Overview
|
||||
const MOCK_ROOMS = [
|
||||
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 } },
|
||||
|
|
@ -27,7 +35,7 @@ const MOCK_ROOMS = [
|
|||
{ 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_ALERTS = [
|
||||
{ id: '1', level: 'CRITICAL', source: 'Flower Room C', message: 'Temperature threshold exceeded (>85°F)', time: '5m ago' },
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
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-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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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
|
||||
<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`}>
|
||||
<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>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h3 className={`font-medium text-sm ${style.text}`}>
|
||||
<h3 className="text-lg font-extra-bold text-slate-900 dark:text-white uppercase italic tracking-tight">
|
||||
{room.name?.replace('[DEMO] ', '')}
|
||||
</h3>
|
||||
<span className="text-[10px] text-tertiary">{room.sqft?.toLocaleString()} sqft • {room.capacity || '—'} cap</span>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-1">
|
||||
{room.sqft?.toLocaleString()} sqft • Capacity {room.capacity || '—'}
|
||||
</p>
|
||||
</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" />
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Batch Count */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-subtle">
|
||||
{/* Batch Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<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'}
|
||||
<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-xs text-accent opacity-0 group-hover:opacity-100 transition-opacity">View →</span>
|
||||
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Enter →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
|
@ -140,3 +208,4 @@ export default function RoomsPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue