feat: Enhance PropertyPanel, add RFID auto-gen, and fix node connections

This commit is contained in:
backuppc
2025-12-22 13:46:01 +09:00
parent df9be853ba
commit 4d1f131d3e
6 changed files with 1995 additions and 1515 deletions

View File

@@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react';
import { SimulationMap, MapNode, MagnetLine, FloorMark } from '../types';
import { Save, Wand2 } from 'lucide-react';
interface PropertyPanelProps {
selectedItemIds: Set<string>;
mapData: SimulationMap;
onUpdate: (type: 'NODE' | 'MAGNET' | 'MARK', id: string, data: any) => void;
}
const PropertyPanel: React.FC<PropertyPanelProps> = ({ selectedItemIds, mapData, onUpdate }) => {
const [formData, setFormData] = useState<any>(null);
const [position, setPosition] = useState({ x: 1100, y: 100 });
const [isDragging, setIsDragging] = useState(false);
const dragOffset = React.useRef({ x: 0, y: 0 });
// When selection changes, update form data
useEffect(() => {
if (selectedItemIds.size === 1) {
const id = Array.from(selectedItemIds)[0];
const node = mapData.nodes.find(n => n.id === id);
const magnet = mapData.magnets.find(m => m.id === id);
const mark = mapData.marks.find(m => m.id === id);
if (node) {
setFormData({ ...node, formType: 'NODE' });
} else if (magnet) {
setFormData({ ...magnet, formType: 'MAGNET' });
} else if (mark) {
setFormData({ ...mark, formType: 'MARK' });
} else {
setFormData(null);
}
} else {
setFormData(null);
}
}, [selectedItemIds, mapData.nodes, mapData.magnets, mapData.marks]);
const handleChange = (field: string, value: string | number | boolean) => {
if (!formData) return;
setFormData((prev: any) => ({ ...prev, [field]: value }));
};
const handleSave = () => {
if (!formData) return;
// Validate RFID Duplication
if (formData.formType === 'NODE' && formData.rfidId && formData.rfidId !== '0000') {
const isDuplicate = mapData.nodes.some(n =>
n.rfidId === formData.rfidId && n.id !== formData.id
);
if (isDuplicate) {
alert(`Error: RFID "${formData.rfidId}" is already used by another node.`);
return;
}
}
onUpdate(formData.formType, formData.id, formData);
};
// Auto Generate RFID
const handleAutoRfid = () => {
const usedRfids = new Set(mapData.nodes.map(n => n.rfidId));
let nextRfid = 1; // Start from 1, assuming 0000 is default/null
while (nextRfid < 10000) {
const candidate = nextRfid.toString().padStart(4, '0');
if (!usedRfids.has(candidate)) {
handleChange('rfidId', candidate);
return;
}
nextRfid++;
}
alert('No available RFID found between 0001 and 9999');
};
// Drag Logic
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
dragOffset.current = {
x: e.clientX - position.x,
y: e.clientY - position.y
};
};
const handleMouseMove = React.useCallback((e: MouseEvent) => {
if (isDragging) {
setPosition({
x: e.clientX - dragOffset.current.x,
y: e.clientY - dragOffset.current.y
});
}
}, [isDragging]);
const handleMouseUp = React.useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
} else {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
if (!formData || selectedItemIds.size !== 1) return null;
return (
<div
style={{
position: 'fixed',
left: position.x,
top: position.y,
zIndex: 100
}}
className="w-64 bg-gray-800 border border-gray-600 rounded-lg shadow-xl overflow-hidden flex flex-col"
>
{/* Header / Drag Handle */}
<div
onMouseDown={handleMouseDown}
className="bg-gray-700 p-2 cursor-move flex items-center justify-between border-b border-gray-600 select-none"
>
<span className="text-gray-200 font-bold text-xs">Properties</span>
<button
onMouseDown={(e) => e.stopPropagation()} // Prevent drag when clicking button
onClick={handleSave}
className="flex items-center gap-1 bg-blue-600 hover:bg-blue-500 text-white px-2 py-0.5 rounded text-[10px] transition-colors"
>
<Save size={12} /> Save
</button>
</div>
<div className="p-4 flex flex-col gap-4 max-h-[400px] overflow-y-auto">
{/* Common Fields */}
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">ID</label>
<input
type="text"
value={formData.id}
disabled
className="bg-gray-900 border border-gray-700 text-gray-400 text-sm p-1 rounded"
/>
</div>
{formData.formType === 'NODE' && (
<>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">Name (Alias)</label>
<input
type="text"
value={formData.name || ''}
onChange={(e) => handleChange('name', e.target.value)}
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">RFID ID</label>
<div className="flex gap-2">
<input
type="text"
value={formData.rfidId || ''}
onChange={(e) => handleChange('rfidId', e.target.value)}
className="flex-1 bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none font-mono"
/>
<button
onClick={handleAutoRfid}
title="Auto Generate Unique RFID"
className="p-1 bg-gray-600 hover:bg-gray-500 text-white rounded transition-colors"
>
<Wand2 size={16} />
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">X</label>
<input
type="number"
value={formData.x}
onChange={(e) => handleChange('x', parseFloat(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">Y</label>
<input
type="number"
value={formData.y}
onChange={(e) => handleChange('y', parseFloat(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-sm p-1 rounded focus:border-blue-500 outline-none"
/>
</div>
</div>
<div className="border-t border-gray-600 my-2 pt-2">
<label className="text-xs font-bold text-gray-300 mb-2 block">Configuration</label>
<div className="grid grid-cols-2 gap-2 mb-2">
<div className="flex flex-col gap-1">
<label className="text-[10px] text-gray-400">Station Type</label>
<input
type="number"
value={formData.stationType || 0}
onChange={(e) => handleChange('stationType', parseInt(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] text-gray-400">Speed Limit</label>
<input
type="number"
value={formData.speedLimit || 0}
onChange={(e) => handleChange('speedLimit', parseInt(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] text-gray-400">Dock Dir</label>
<input
type="number"
value={formData.dockDirection || 0}
onChange={(e) => handleChange('dockDirection', parseInt(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] text-gray-400">Text Size</label>
<input
type="number"
value={formData.nodeTextFontSize || 12}
onChange={(e) => handleChange('nodeTextFontSize', parseInt(e.target.value))}
className="bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded"
/>
</div>
</div>
<div className="flex flex-col gap-1 mb-2">
<label className="text-[10px] text-gray-400">Text Color</label>
<div className="flex gap-2">
<input
type="color"
value={formData.nodeTextForeColor || '#FFFFFF'}
onChange={(e) => handleChange('nodeTextForeColor', e.target.value)}
className="bg-transparent w-6 h-6 p-0 border-0"
/>
<input
type="text"
value={formData.nodeTextForeColor || '#FFFFFF'}
onChange={(e) => handleChange('nodeTextForeColor', e.target.value)}
className="flex-1 bg-gray-700 border border-gray-600 text-gray-200 text-xs p-1 rounded font-mono"
/>
</div>
</div>
<div className="flex flex-col gap-2 mt-2">
{[
{ k: 'canDocking', l: 'Can Docking' },
{ k: 'canTurnLeft', l: 'Can Turn Left' },
{ k: 'canTurnRight', l: 'Can Turn Right' },
{ k: 'disableCross', l: 'Disable Cross' },
{ k: 'isActive', l: 'Is Active' },
].map((item) => (
<label key={item.k} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={!!formData[item.k]}
onChange={(e) => handleChange(item.k, e.target.checked ? true : false)}
className="rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-0"
/>
<span className="text-xs text-gray-300">{item.l}</span>
</label>
))}
</div>
</div>
</>
)}
</div>
</div>
);
};
export default PropertyPanel;