ca-grow-ops-manager/frontend/src/components/layout-editor/RackConfigModal.tsx
fullsizemalt 6d957f1c92
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
refactor(ui): theme harmonization and semantic tokens
2026-01-01 19:05:26 -08:00

222 lines
11 KiB
TypeScript

/**
* RackConfigModal - Modal for configuring rack/section dimensions
*/
import { useState, useEffect } from 'react';
import { Table, Server, Box, GripVertical, Square, X } from 'lucide-react';
import { cn } from '../../lib/utils';
import type { RackData } from './RackVisualizer';
interface RackConfigModalProps {
rack: RackData;
isOpen: boolean;
onClose: () => void;
onSave: (updates: Partial<RackData>) => void;
}
const SUBTYPE_OPTIONS = [
{ value: 'TABLE', label: 'Table', Icon: Table, desc: 'Flat growing surface' },
{ value: 'RACK', label: 'Rack', Icon: Server, desc: 'Multi-tier vertical rack' },
{ value: 'TRAY', label: 'Tray', Icon: Box, desc: 'Flood/drain tray system' },
{ value: 'HANGER', label: 'Hanger', Icon: GripVertical, desc: 'Hanging/drying racks' },
{ value: 'FLOOR', label: 'Floor', Icon: Square, desc: 'Ground-level positions' },
];
export function RackConfigModal({ rack, isOpen, onClose, onSave }: RackConfigModalProps) {
const [name, setName] = useState(rack.name);
const [code, setCode] = useState(rack.code || '');
const [subtype, setSubtype] = useState(rack.subtype);
const [rows, setRows] = useState(rack.rows);
const [columns, setColumns] = useState(rack.columns);
const [tiers, setTiers] = useState(rack.tiers);
const [saving, setSaving] = useState(false);
// Reset form when rack changes
useEffect(() => {
setName(rack.name);
setCode(rack.code || '');
setSubtype(rack.subtype);
setRows(rack.rows);
setColumns(rack.columns);
setTiers(rack.tiers);
}, [rack]);
if (!isOpen) return null;
const handleSave = async () => {
setSaving(true);
try {
await onSave({
name,
code: code || undefined,
subtype,
rows,
columns,
tiers,
});
onClose();
} catch (e) {
console.error('Failed to save rack config:', e);
} finally {
setSaving(false);
}
};
const totalSlots = rows * columns * tiers;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
{/* Modal */}
<div className="relative bg-card rounded-xl shadow-2xl w-full max-w-lg mx-4 border border-border animate-in scale-in">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<h2 className="text-lg font-semibold text-foreground">Configure Section</h2>
<button
onClick={onClose}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="px-6 py-4 space-y-5">
{/* Name & Code */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-foreground mb-1">Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-secondary/50 border border-input rounded text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all font-sans"
placeholder="e.g., Table 1"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Code</label>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
className="w-full px-3 py-2 bg-secondary/50 border border-input rounded text-foreground focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all font-mono"
placeholder="e.g., T1"
/>
</div>
</div>
{/* Section Type */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">Section Type</label>
<div className="grid grid-cols-5 gap-2">
{SUBTYPE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => setSubtype(opt.value as RackData['subtype'])}
className={cn(
'flex flex-col items-center p-2 rounded border transition-colors',
subtype === opt.value
? 'border-primary bg-primary/10 text-primary'
: 'border-border bg-secondary/30 text-muted-foreground hover:border-border-strong hover:text-foreground'
)}
title={opt.desc}
>
<opt.Icon className="w-6 h-6 mb-1" />
<span className="text-xs">{opt.label}</span>
</button>
))}
</div>
</div>
{/* Dimensions */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">Dimensions</label>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-muted-foreground mb-1">Rows</label>
<input
type="number"
value={rows}
onChange={(e) => setRows(Math.max(1, Math.min(50, parseInt(e.target.value) || 1)))}
min={1}
max={50}
className="w-full px-3 py-2 bg-secondary/50 border border-input rounded text-foreground text-center focus:outline-none focus:border-primary font-mono"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Columns</label>
<input
type="number"
value={columns}
onChange={(e) => setColumns(Math.max(1, Math.min(50, parseInt(e.target.value) || 1)))}
min={1}
max={50}
className="w-full px-3 py-2 bg-secondary/50 border border-input rounded text-foreground text-center focus:outline-none focus:border-primary font-mono"
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1">Tiers</label>
<input
type="number"
value={tiers}
onChange={(e) => setTiers(Math.max(1, Math.min(10, parseInt(e.target.value) || 1)))}
min={1}
max={10}
className="w-full px-3 py-2 bg-secondary/50 border border-input rounded text-foreground text-center focus:outline-none focus:border-primary font-mono"
/>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
Total positions: <span className="text-primary font-mono">{totalSlots}</span>
</p>
</div>
{/* Visual Preview */}
<div className="bg-secondary/20 rounded p-4 border border-border/50">
<p className="text-xs text-muted-foreground mb-2">Preview</p>
<div
className="grid gap-1"
style={{
gridTemplateColumns: `repeat(${Math.min(columns, 10)}, 1fr)`,
maxWidth: '200px',
}}
>
{Array.from({ length: Math.min(rows * columns, 50) }, (_, i) => (
<div
key={i}
className="aspect-square bg-secondary border border-dashed border-border rounded-sm"
style={{ minWidth: '12px', minHeight: '12px' }}
/>
))}
</div>
{rows * columns > 50 && (
<p className="text-xs text-muted-foreground mt-1 italic">Showing first 50 slots</p>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 px-6 py-4 border-t border-border bg-secondary/10">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={saving || !name.trim()}
className="px-4 py-2 text-sm bg-primary hover:bg-primary-hover disabled:bg-primary/50 disabled:cursor-not-allowed text-primary-foreground rounded transition-colors font-medium shadow-sm hover:shadow-md"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
</div>
);
}
export default RackConfigModal;