feat: Add real-time IO/interlock updates, HW status display, and history page
- Implement real-time IO value updates via IOValueChanged event - Add interlock toggle and real-time interlock change events - Fix ToggleLight to check return value of DIO.SetRoomLight - Add HW status display in Footer matching WinForms HWState - Implement GetHWStatus API and 250ms broadcast interval - Create HistoryPage React component for work history viewing - Add GetHistoryData API for database queries - Add date range selection, search, filter, and CSV export - Add History button in Header navigation - Add PickerMoveDialog component for manage operations - Fix DataSet column names (idx, PRNATTACH, PRNVALID, qtymax) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Package, X, ChevronRight } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { useAlert } from '../contexts/AlertContext';
|
||||
import { usePickerMove } from '../App';
|
||||
|
||||
interface FunctionMenuProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,6 +13,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { showAlert } = useAlert();
|
||||
const { openPickerMove } = usePickerMove();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -29,8 +31,6 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCommand = async (commandFn: () => Promise<{ success: boolean; message: string }>, actionName: string) => {
|
||||
try {
|
||||
const result = await commandFn();
|
||||
@@ -55,6 +55,8 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute top-20 left-1/2 -translate-x-1/2 z-50 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[300px]"
|
||||
@@ -77,7 +79,12 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
<div className="p-2">
|
||||
{/* Manage */}
|
||||
<button
|
||||
onClick={() => handleCommand(() => comms.openManage(), 'Manage')}
|
||||
onClick={async () => {
|
||||
const success = await openPickerMove();
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech"
|
||||
>
|
||||
<span>Manage</span>
|
||||
@@ -136,5 +143,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
]);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [statusInterval, setStatusInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [statusInterval, setStatusInterval] = useState<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Reset state when modal closes
|
||||
useEffect(() => {
|
||||
|
||||
446
FrontEnd/components/PickerMoveDialog.tsx
Normal file
446
FrontEnd/components/PickerMoveDialog.tsx
Normal file
@@ -0,0 +1,446 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Move, Square, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Home, Printer, XCircle, Settings } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { useAlert } from '../contexts/AlertContext';
|
||||
|
||||
interface PickerMoveDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface PickerStatus {
|
||||
xEnabled: boolean;
|
||||
zEnabled: boolean;
|
||||
pickerSafe: boolean;
|
||||
managementEnabled: boolean;
|
||||
manPosL: boolean;
|
||||
manPosR: boolean;
|
||||
}
|
||||
|
||||
export const PickerMoveDialog: React.FC<PickerMoveDialogProps> = ({ isOpen, onClose }) => {
|
||||
const { showAlert, showConfirm } = useAlert();
|
||||
const [status, setStatus] = useState<PickerStatus>({
|
||||
xEnabled: false,
|
||||
zEnabled: false,
|
||||
pickerSafe: false,
|
||||
managementEnabled: false,
|
||||
manPosL: false,
|
||||
manPosR: false
|
||||
});
|
||||
const [isJogging, setIsJogging] = useState(false);
|
||||
|
||||
// Subscribe to picker status updates
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const unsubscribe = comms.subscribe((data: any) => {
|
||||
if (data.type === 'PICKER_STATUS') {
|
||||
setStatus(data.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Request initial status
|
||||
comms.getPickerStatus();
|
||||
|
||||
// Set up polling interval
|
||||
const intervalId = setInterval(() => {
|
||||
comms.getPickerStatus();
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
const result = await comms.canCloseManage();
|
||||
if (!result.canClose) {
|
||||
showAlert({
|
||||
type: 'error',
|
||||
title: 'Cannot Close',
|
||||
message: result.message || 'Printer motion is in management position.\nReturn to position and try again.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
// If check fails, still allow close
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// === Position Move Buttons ===
|
||||
const handleMoveLeft = async () => {
|
||||
const result = await comms.pickerMoveLeft();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveLeftWait = async () => {
|
||||
const result = await comms.pickerMoveLeftWait();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveCenter = async () => {
|
||||
const result = await comms.pickerMoveCenter();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveRightWait = async () => {
|
||||
const result = await comms.pickerMoveRightWait();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveRight = async () => {
|
||||
const result = await comms.pickerMoveRight();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
// === Jog Control ===
|
||||
const handleJogStart = useCallback(async (direction: 'up' | 'down' | 'left' | 'right') => {
|
||||
setIsJogging(true);
|
||||
await comms.pickerJogStart(direction);
|
||||
}, []);
|
||||
|
||||
const handleJogStop = useCallback(async () => {
|
||||
setIsJogging(false);
|
||||
await comms.pickerJogStop();
|
||||
}, []);
|
||||
|
||||
const handleStop = async () => {
|
||||
await comms.pickerStop();
|
||||
};
|
||||
|
||||
// === Vision Validation Cancel ===
|
||||
const handleCancelVisionL = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Cancel Vision Validation',
|
||||
message: 'Do you want to cancel LEFT-QR code verification?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.cancelVisionValidation('left');
|
||||
if (result.success) {
|
||||
showAlert({ type: 'success', title: 'Cancelled', message: 'LEFT-QR verification cancelled' });
|
||||
} else {
|
||||
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelVisionR = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Cancel Vision Validation',
|
||||
message: 'Do you want to cancel RIGHT-QR code verification?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.cancelVisionValidation('right');
|
||||
if (result.success) {
|
||||
showAlert({ type: 'success', title: 'Cancelled', message: 'RIGHT-QR verification cancelled' });
|
||||
} else {
|
||||
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === Management Position ===
|
||||
const handleManagePosL = async () => {
|
||||
const result = await comms.pickerManagePosition('left');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePosR = async () => {
|
||||
const result = await comms.pickerManagePosition('right');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleManagePosReturn = async () => {
|
||||
const result = await comms.pickerManageReturn();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Return Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
// === Z-Axis Control ===
|
||||
const handleZHome = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Z-Axis Home',
|
||||
message: 'Do you want to proceed with picker Z-axis home search?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.pickerZHome();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Home Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleZZero = async () => {
|
||||
const confirmed = await showConfirm({
|
||||
title: 'Z-Axis Zero',
|
||||
message: 'Do you want to move picker Z-axis to coordinate:0?'
|
||||
});
|
||||
if (confirmed) {
|
||||
const result = await comms.pickerZZero();
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === Print Test ===
|
||||
const handlePrintL = async () => {
|
||||
const result = await comms.pickerTestPrint('left');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrintR = async () => {
|
||||
const result = await comms.pickerTestPrint('right');
|
||||
if (!result.success) {
|
||||
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative bg-black/95 backdrop-blur-md border-2 border-neon-blue rounded-lg shadow-2xl w-full max-w-4xl mx-4 animate-fadeIn">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Move className="w-6 h-6 text-neon-blue" />
|
||||
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
|
||||
Picker(X) Movement and Management
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
{/* Row 1: Jog Controls */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
{/* Z Up */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('up')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.zEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
|
||||
>
|
||||
<ChevronUp className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* X Left Jog */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('left')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.xEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* Stop */}
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className={`h-24 flex items-center justify-center border border-white/10 rounded-lg transition-colors ${
|
||||
status.pickerSafe
|
||||
? 'bg-green-500/30 hover:bg-green-500/40 text-green-400'
|
||||
: 'bg-red-500/30 hover:bg-red-500/40 text-red-400'
|
||||
}`}
|
||||
>
|
||||
<Square className="w-16 h-16" fill="currentColor" />
|
||||
</button>
|
||||
|
||||
{/* X Right Jog */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('right')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.xEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
|
||||
{/* Z Down */}
|
||||
<button
|
||||
onMouseDown={() => handleJogStart('down')}
|
||||
onMouseUp={handleJogStop}
|
||||
onMouseLeave={handleJogStop}
|
||||
disabled={!status.zEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
|
||||
>
|
||||
<ChevronDown className="w-16 h-16" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Position Buttons */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
<button
|
||||
onClick={handleMoveLeft}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveLeftWait}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Wait
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveCenter}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-green-400 font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveRightWait}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Wait
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveRight}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Vision Cancel & Management Position */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-2">
|
||||
<button
|
||||
onClick={handleCancelVisionL}
|
||||
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<XCircle className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Vision Validation</span>
|
||||
<span className="text-sm">Cancel(L)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosL}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Settings className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Print Management</span>
|
||||
<span className="text-sm">Position(L)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosReturn}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Home className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Management Position</span>
|
||||
<span className="text-sm">Return</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManagePosR}
|
||||
disabled={!status.managementEnabled}
|
||||
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<Settings className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Print Management</span>
|
||||
<span className="text-sm">Position(R)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelVisionR}
|
||||
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
|
||||
>
|
||||
<XCircle className="w-6 h-6 mb-1" />
|
||||
<span className="text-sm">Vision Validation</span>
|
||||
<span className="text-sm">Cancel(R)</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Z-Home, Print, Z-Zero */}
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
<button
|
||||
onClick={handleZHome}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Home className="w-6 h-6 mb-1" />
|
||||
<span>Z-HOME</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintL}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Printer className="w-6 h-6 mb-1" />
|
||||
<span>PRINT(L)</span>
|
||||
</button>
|
||||
<button
|
||||
disabled
|
||||
className="h-24 flex items-center justify-center bg-slate-800/30 border border-white/10 rounded-lg text-slate-500 font-tech text-lg font-bold cursor-not-allowed"
|
||||
>
|
||||
--
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintR}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<Printer className="w-6 h-6 mb-1" />
|
||||
<span>PRINT(R)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleZZero}
|
||||
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
|
||||
>
|
||||
<span className="text-2xl mb-1">0</span>
|
||||
<span>Z-ZERO</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end p-4 border-t border-white/10">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-6 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 text-white font-tech font-bold rounded transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) => (
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-white/10 pb-2">
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon, className, children }) => (
|
||||
<div className={`flex items-center gap-3 mb-6 border-b border-white/10 pb-2 ${className || ''}`}>
|
||||
<div className="text-neon-blue animate-pulse">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -15,5 +17,6 @@ export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) =
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-neon-blue/50 to-transparent"></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RobotTarget } from '../../types';
|
||||
import { comms } from '../../communication';
|
||||
|
||||
// HW 상태 타입 (윈폼 HWState와 동일)
|
||||
// status: 0=SET(미설정/회색), 1=ON(연결/녹색), 2=TRIG(트리거/노란색), 3=OFF(연결안됨/빨간색)
|
||||
interface HWItem {
|
||||
name: string;
|
||||
title: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
isHostConnected: boolean;
|
||||
robotTarget: RobotTarget;
|
||||
}
|
||||
|
||||
// 상태에 따른 LED 색상 반환
|
||||
const getStatusColor = (status: number): { bg: string; shadow: string; text: string } => {
|
||||
switch (status) {
|
||||
case 1: // ON - 녹색
|
||||
return { bg: 'bg-neon-green', shadow: 'shadow-[0_0_5px_#0aff00]', text: 'text-green-400' };
|
||||
case 2: // TRIG - 노란색
|
||||
return { bg: 'bg-yellow-400', shadow: 'shadow-[0_0_5px_#facc15]', text: 'text-yellow-400' };
|
||||
case 3: // OFF - 빨간색
|
||||
return { bg: 'bg-red-500', shadow: 'shadow-[0_0_5px_#ef4444]', text: 'text-red-400' };
|
||||
default: // SET - 회색 (미설정)
|
||||
return { bg: 'bg-gray-500', shadow: '', text: 'text-gray-400' };
|
||||
}
|
||||
};
|
||||
|
||||
export const Footer: React.FC<FooterProps> = ({ isHostConnected, robotTarget }) => {
|
||||
const [hwStatus, setHwStatus] = useState<HWItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// HW_STATUS_UPDATE 이벤트 구독
|
||||
const unsubscribe = comms.subscribe((msg: any) => {
|
||||
if (msg?.type === 'HW_STATUS_UPDATE' && msg.data) {
|
||||
setHwStatus(msg.data);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className="absolute bottom-0 left-0 right-0 h-10 bg-black/80 border-t border-neon-blue/30 flex items-center px-6 justify-between z-40 backdrop-blur text-xs font-mono text-slate-400">
|
||||
<div className="flex gap-6">
|
||||
{['PLC', 'MOTION', 'VISION', 'LIGHT'].map(hw => (
|
||||
<div key={hw} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-neon-green rounded-full shadow-[0_0_5px_#0aff00]"></div>
|
||||
<span className="font-bold text-slate-300">{hw}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-4">
|
||||
{/* H/W 상태 표시 (윈폼 HWState와 동일) */}
|
||||
{hwStatus.map((hw) => {
|
||||
const colors = getStatusColor(hw.status);
|
||||
return (
|
||||
<div key={hw.name} className="flex items-center gap-1.5" title={`${hw.name}: ${hw.title}`}>
|
||||
<div className={`w-2 h-2 rounded-full transition-all ${colors.bg} ${colors.shadow}`}></div>
|
||||
<span className={`font-bold ${colors.text}`}>{hw.name}</span>
|
||||
<span className="text-[10px] text-slate-500">{hw.title}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* HOST 연결 상태 */}
|
||||
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-slate-700">
|
||||
<div className={`w-2 h-2 rounded-full transition-all ${isHostConnected ? 'bg-neon-green shadow-[0_0_5px_#0aff00]' : 'bg-red-500 shadow-[0_0_5px_#ff0000] animate-pulse'}`}></div>
|
||||
<span className={`font-bold ${isHostConnected ? 'text-slate-300' : 'text-red-400'}`}>HOST</span>
|
||||
<span className={`font-bold ${isHostConnected ? 'text-green-400' : 'text-red-400'}`}>HOST</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-8 text-neon-blue">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen } from 'lucide-react';
|
||||
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen, History, Clock } from 'lucide-react';
|
||||
import { VisionMenu } from '../VisionMenu';
|
||||
import { FunctionMenu } from '../FunctionMenu';
|
||||
import { ManualPrintDialog, PrintData } from '../ManualPrintDialog';
|
||||
@@ -127,6 +127,20 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="leading-tight">CANCEL</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate('/history');
|
||||
onTabChange(null);
|
||||
}}
|
||||
className={`flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-xl font-tech font-bold text-[10px] transition-all border border-transparent min-w-[70px] ${location.pathname === '/history'
|
||||
? 'bg-neon-blue/10 text-neon-blue border-neon-blue shadow-glow-blue'
|
||||
: 'text-slate-400 hover:text-purple-400 hover:bg-white/5'
|
||||
}`}
|
||||
title="View Work History"
|
||||
>
|
||||
<Clock className="w-5 h-5" />
|
||||
<span className="leading-tight">HISTORY</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Navigation */}
|
||||
|
||||
Reference in New Issue
Block a user