222 lines
11 KiB
TypeScript
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;
|