ca-grow-ops-manager/frontend/src/pages/RoomsPage.tsx
fullsizemalt e88814afef
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix: Expose Generate Zone button and refine styling (remove italics/caps, improve contrast)
2025-12-27 15:03:36 -08:00

236 lines
14 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Home, Plus, Thermometer, Droplets, ChevronRight, Activity, Leaf, Flower, ShieldCheck, ArrowRight, Wand2 } from 'lucide-react';
import api from '../lib/api';
import { usePermissions } from '../hooks/usePermissions';
import { Card } from '../components/ui/card';
import { cn } from '../lib/utils';
import { RoomLayoutWizard } from '../components/layout/RoomLayoutWizard';
export default function RoomsPage() {
const { isManager } = usePermissions();
const [rooms, setRooms] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isWizardOpen, setIsWizardOpen] = useState(false);
useEffect(() => {
fetchRooms();
}, []);
const fetchRooms = async () => {
setIsLoading(true);
try {
const { data } = await api.get('/rooms');
setRooms(data);
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
};
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] || { 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-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-bold tracking-tight text-[var(--color-text-primary)]">
Cultivation Zones
</h1>
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)] text-xs font-medium">
<ShieldCheck size={14} className="text-[var(--color-primary)]" />
<span>{rooms.length} Active Zones Environmental Controls Active</span>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => setIsWizardOpen(true)}
className="flex items-center gap-2 bg-[var(--color-primary)]/10 hover:bg-[var(--color-primary)]/20 text-[var(--color-primary)] border border-[var(--color-primary)]/20 px-4 py-2.5 rounded-xl font-bold text-sm shadow-sm transition-all"
>
<Wand2 size={18} />
Generate Zone
</button>
{isManager && (
<button className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all">
<Plus size={18} />
Add Zone
</button>
)}
</div>
</div>
{/* KPI Strip */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-4 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">Total Zones</p>
<p className="text-xl font-bold text-[var(--color-text-primary)]">{rooms.length}</p>
</div>
<div className="p-2.5 rounded-lg border bg-[var(--color-accent)]/5 text-[var(--color-accent)] border-blue-500/10">
<Home size={16} />
</div>
</Card>
<Card className="p-4 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">Veg Rooms</p>
<p className="text-xl font-bold text-[var(--color-text-primary)]">{vegCount}</p>
</div>
<div className="p-2.5 rounded-lg border bg-[var(--color-primary)]/5 text-[var(--color-primary)] border-emerald-500/10">
<Leaf size={16} />
</div>
</Card>
<Card className="p-4 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">Flower Rooms</p>
<p className="text-xl font-bold text-[var(--color-text-primary)]">{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 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] flex items-center justify-between">
<div className="space-y-1">
<p className="text-[9px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">Active Batches</p>
<p className="text-xl font-bold text-[var(--color-text-primary)]">{totalBatches}</p>
</div>
<div className="p-2.5 rounded-lg border bg-[var(--color-warning)]/5 text-[var(--color-warning)] 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) => (
<Card key={i} className="p-6 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] 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 ? (
<div className="py-16 text-center rounded-2xl border-2 border-dashed border-[var(--color-border-subtle)]">
<Home size={40} className="mx-auto text-slate-300 dark:text-slate-700 mb-4" />
<p className="text-sm font-bold text-[var(--color-text-tertiary)] uppercase tracking-widest mb-4">No Cultivation Zones Configured</p>
{isManager && (
<div className="flex gap-4 justify-center">
<button
onClick={() => setIsWizardOpen(true)}
className="flex items-center gap-2 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/50 px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"
>
<Wand2 size={16} /> Generate Zone
</button>
<button className="btn btn-primary mt-4">
<Plus size={16} /> Create Custom
</button>
</div>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{rooms.map(room => {
const { accent, icon: Icon } = getRoomStyle(room.type);
const accentClasses = {
emerald: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)] border-emerald-500/20',
purple: 'bg-purple-500/10 text-purple-500 border-purple-500/20',
amber: 'bg-[var(--color-warning)]/10 text-[var(--color-warning)] 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-[var(--color-text-tertiary)] 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="group block"
>
<Card className="p-0 overflow-hidden bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] hover:border-emerald-500/50 transition-all">
{/* Header */}
<div className={cn("px-5 py-4 border-b border-[var(--color-border-subtle)] flex items-center justify-between", accentClasses[accent as keyof typeof accentClasses])}>
<div className="flex items-center gap-3">
<Icon size={16} />
<span className="text-xs font-bold">{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="text-lg font-bold text-[var(--color-text-primary)]">
{room.name?.replace('[DEMO] ', '')}
</h3>
<p className="text-xs font-medium text-[var(--color-text-tertiary)] 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-[var(--color-border-subtle)]">
<div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-xl font-bold tracking-tight text-[var(--color-text-primary)]">
<Thermometer size={14} className="text-[var(--color-error)]" />
{room.targetTemp || '—'}°F
</div>
<span className="text-xs font-medium text-[var(--color-text-tertiary)]">Target Temp</span>
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-1.5 text-xl font-bold tracking-tight text-[var(--color-text-primary)]">
<Droplets size={14} className="text-[var(--color-accent)]" />
{room.targetHumidity || '—'}%
</div>
<span className="text-xs font-medium text-[var(--color-text-tertiary)]">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-[var(--color-primary)]' : 'bg-slate-300 dark:bg-slate-700')} />
<span className="text-xs font-bold text-[var(--color-text-secondary)]">
{room.batches?.length || 0} Active Batch{room.batches?.length === 1 ? '' : 'es'}
</span>
</div>
<span className="text-xs font-bold text-[var(--color-primary)] opacity-0 group-hover:opacity-100 transition-opacity">
Enter
</span>
</div>
</div>
</Card>
</Link>
);
})}
</div>
)}
<RoomLayoutWizard
isOpen={isWizardOpen}
onClose={() => setIsWizardOpen(false)}
onSuccess={fetchRooms}
floorId="default-floor-id"
/>
</div>
);
}