- Room cards now have colored header backgrounds per type (VEG=green, FLOWER=purple, DRY=amber, CURE=orange, etc.) - Cards are clickable, linking to /rooms/:id - New RoomDetailPage with gradient header, sensor metrics with sparklines, active batches list - Backend: GET /rooms/:id endpoint returns room with batches
142 lines
7.6 KiB
TypeScript
142 lines
7.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Home, Plus, Thermometer, Droplets, ChevronRight } from 'lucide-react';
|
|
import api from '../lib/api';
|
|
import { usePermissions } from '../hooks/usePermissions';
|
|
import { PageHeader, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
// 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' },
|
|
};
|
|
return styles[type] || { bg: 'bg-tertiary', text: 'text-secondary', border: 'border-subtle' };
|
|
};
|
|
|
|
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>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{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} />)}
|
|
</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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{rooms.map(room => {
|
|
const style = getHeaderStyle(room.type);
|
|
return (
|
|
<Link
|
|
key={room.id}
|
|
to={`/rooms/${room.id}`}
|
|
className="card overflow-hidden group hover:shadow-md transition-shadow"
|
|
>
|
|
{/* 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>
|
|
</div>
|
|
</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'}
|
|
</span>
|
|
</div>
|
|
<span className="text-xs text-accent opacity-0 group-hover:opacity-100 transition-opacity">View →</span>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|