- Boost text contrast in both themes - Strengthen border visibility (subtle borders now visible) - Convert 39 files from hardcoded dark:/light: to CSS vars - Tertiary text now more readable on both backgrounds
211 lines
13 KiB
TypeScript
211 lines
13 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
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 { Card } from '../components/ui/card';
|
|
import { cn } from '../lib/utils';
|
|
|
|
export default function RoomsPage() {
|
|
const { isManager } = usePermissions();
|
|
const [rooms, setRooms] = useState<any[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
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-extra-bold tracking-tight text-[var(--color-text-primary)] uppercase italic">
|
|
Cultivation Zones
|
|
</h1>
|
|
<div className="flex items-center gap-2 text-[var(--color-text-tertiary)] text-xs font-bold uppercase tracking-widest">
|
|
<ShieldCheck size={14} className="text-[var(--color-primary)]" />
|
|
<span>{rooms.length} Active Zones • Environmental Controls Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
{isManager && (
|
|
<button className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] 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 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-black text-[var(--color-text-tertiary)] uppercase tracking-widest">Total Zones</p>
|
|
<p className="text-xl font-black italic tracking-tighter 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-black text-[var(--color-text-tertiary)] uppercase tracking-widest">Veg Rooms</p>
|
|
<p className="text-xl font-black italic tracking-tighter 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-black text-[var(--color-text-tertiary)] uppercase tracking-widest">Flower Rooms</p>
|
|
<p className="text-xl font-black italic tracking-tighter 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-black text-[var(--color-text-tertiary)] uppercase tracking-widest">Active Batches</p>
|
|
<p className="text-xl font-black italic tracking-tighter 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 && (
|
|
<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-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-[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="text-lg font-extra-bold text-[var(--color-text-primary)] uppercase italic tracking-tight">
|
|
{room.name?.replace('[DEMO] ', '')}
|
|
</h3>
|
|
<p className="text-[10px] font-bold text-[var(--color-text-tertiary)] 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-[var(--color-border-subtle)]">
|
|
<div className="text-center">
|
|
<div className="flex items-center justify-center gap-1.5 text-xl font-black tracking-tighter text-[var(--color-text-primary)]">
|
|
<Thermometer size={14} className="text-[var(--color-error)]" />
|
|
{room.targetTemp || '—'}°F
|
|
</div>
|
|
<span className="text-[9px] font-bold text-[var(--color-text-tertiary)] 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-[var(--color-text-primary)]">
|
|
<Droplets size={14} className="text-[var(--color-accent)]" />
|
|
{room.targetHumidity || '—'}%
|
|
</div>
|
|
<span className="text-[9px] font-bold text-[var(--color-text-tertiary)] 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-[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-[10px] font-bold text-[var(--color-primary)] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">
|
|
Enter →
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|