ca-grow-ops-manager/frontend/src/pages/RoomsPage.tsx
fullsizemalt eb5ebc610f
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
feat: room cards with color-coded headers + room detail page
- 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
2025-12-12 19:33:07 -08:00

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>
);
}