feat: Implement manual print dialog with full label printing functionality

Added complete manual print dialog to Web UI based on fManualPrint.cs:
- Created ManualPrintDialog component with all input fields (SID, Vendor Lot, Qty, MFG Date, Reel ID, Supplier, Part No)
- Added printer selection (Left/Right), print count, delete after print checkbox, and barcode input
- Implemented ExecuteManualPrint backend method with ZPL label generation and printer integration
- Added WebSocketServer handler for EXECUTE_MANUAL_PRINT command with full parameter support
- Integrated dialog into Header component with proper error handling via AlertContext

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-25 23:48:18 +09:00
parent 8bbd76e670
commit bb67d04d90
5 changed files with 334 additions and 13 deletions

View File

@@ -525,7 +525,17 @@ class CommunicationLayer {
}
}
public async openManualPrint(): Promise<{ success: boolean; message: string }> {
public async executeManualPrint(printData: {
sid: string;
venderLot: string;
qty: string;
mfg: string;
rid: string;
spy: string;
partNo: string;
printer: string;
count: number;
}): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Manual print not yet implemented in WebView2 mode' };
} else {
@@ -533,7 +543,7 @@ class CommunicationLayer {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Manual print timeout'));
}, 5000);
}, 10000);
const handler = (data: any) => {
if (data.type === 'MANUAL_PRINT_RESULT') {
@@ -544,7 +554,10 @@ class CommunicationLayer {
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'OPEN_MANUAL_PRINT' }));
this.ws?.send(JSON.stringify({
type: 'EXECUTE_MANUAL_PRINT',
...printData
}));
});
}
}

View File

@@ -0,0 +1,246 @@
import React, { useState } from 'react';
import { X, Printer } from 'lucide-react';
import { useAlert } from '../contexts/AlertContext';
interface ManualPrintDialogProps {
isOpen: boolean;
onClose: () => void;
onPrint: (data: PrintData) => Promise<{ success: boolean; message: string }>;
}
export interface PrintData {
sid: string;
venderLot: string;
qty: string;
mfg: string;
rid: string;
spy: string;
partNo: string;
printer: 'left' | 'right';
count: number;
deleteAfterPrint: boolean;
}
export const ManualPrintDialog: React.FC<ManualPrintDialogProps> = ({ isOpen, onClose, onPrint }) => {
const [sid, setSid] = useState('');
const [venderLot, setVenderLot] = useState('');
const [qty, setQty] = useState('');
const [mfg, setMfg] = useState('');
const [rid, setRid] = useState('');
const [spy, setSpy] = useState('');
const [partNo, setPartNo] = useState('');
const [printer, setPrinter] = useState<'left' | 'right'>('right');
const [count, setCount] = useState(1);
const [deleteAfterPrint, setDeleteAfterPrint] = useState(false);
const [barcodeInput, setBarcodeInput] = useState('');
const { showAlert } = useAlert();
if (!isOpen) return null;
const handlePrint = async () => {
// Validate qty is a number
if (qty && isNaN(Number(qty))) {
showAlert({
type: 'error',
title: 'Invalid Input',
message: 'Please enter quantity as a number'
});
return;
}
const printData: PrintData = {
sid,
venderLot,
qty,
mfg,
rid,
spy,
partNo,
printer,
count,
deleteAfterPrint
};
try {
const result = await onPrint(printData);
if (result.success) {
showAlert({
type: 'success',
title: 'Print Success',
message: result.message
});
if (deleteAfterPrint) {
setSid('');
setVenderLot('');
setQty('');
setMfg('');
setRid('');
setSpy('');
setPartNo('');
}
} else {
showAlert({
type: 'error',
title: 'Print Failed',
message: result.message
});
}
} catch (error: any) {
showAlert({
type: 'error',
title: 'Print Error',
message: error.message || 'Unknown error'
});
}
};
const clearField = (setter: React.Dispatch<React.SetStateAction<string>>) => {
setter('');
};
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={onClose}
/>
{/* Dialog */}
<div className="relative bg-black/95 backdrop-blur-md border-2 border-neon-blue rounded-lg shadow-2xl w-full max-w-2xl mx-4 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/10">
<div className="flex items-center gap-3">
<Printer className="w-6 h-6 text-neon-blue" />
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
MANUAL PRINT
</h2>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Content */}
<div className="p-6 max-h-[70vh] overflow-y-auto">
{/* Printer Selection */}
<div className="bg-slate-800/50 border border-white/10 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between">
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
value="left"
checked={printer === 'left'}
onChange={(e) => setPrinter(e.target.value as 'left')}
className="w-4 h-4 text-neon-blue"
/>
<span className="text-white font-tech">Left</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
value="right"
checked={printer === 'right'}
onChange={(e) => setPrinter(e.target.value as 'right')}
className="w-4 h-4 text-neon-blue"
/>
<span className="text-white font-tech">Right</span>
</label>
</div>
<div className="flex items-center gap-4">
<label className="text-slate-400 font-tech text-sm">Count</label>
<input
type="number"
min="1"
max="99999"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
className="w-24 px-3 py-1 bg-slate-900 border border-white/20 rounded text-white text-center font-mono"
/>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={deleteAfterPrint}
onChange={(e) => setDeleteAfterPrint(e.target.checked)}
className="w-4 h-4 text-neon-blue"
/>
<span className="text-slate-400 font-tech text-sm">Delete after printing</span>
</label>
</div>
</div>
</div>
{/* Input Fields */}
<div className="space-y-3">
{[
{ label: 'SID', value: sid, setter: setSid },
{ label: 'Vendor Lot', value: venderLot, setter: setVenderLot },
{ label: 'Quantity', value: qty, setter: setQty },
{ label: 'MFG Date', value: mfg, setter: setMfg },
{ label: 'Reel ID', value: rid, setter: setRid },
{ label: 'Supplier', value: spy, setter: setSpy },
{ label: 'Part No', value: partNo, setter: setPartNo },
].map((field) => (
<div key={field.label} className="flex items-center gap-3">
<label className="w-32 text-slate-400 font-tech text-sm">
{field.label}
</label>
<input
type="text"
value={field.value}
onChange={(e) => field.setter(e.target.value)}
className="flex-1 px-4 py-2 bg-slate-900 border border-white/20 rounded text-white font-mono focus:border-neon-blue focus:outline-none"
/>
<button
onClick={() => clearField(field.setter)}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 rounded text-white font-tech text-sm transition-colors"
>
Clear
</button>
</div>
))}
</div>
{/* Barcode Input */}
<div className="mt-6 p-4 bg-slate-800/50 border border-white/10 rounded-lg">
<label className="block text-slate-400 font-tech text-sm mb-2">
Barcode input field (move cursor and enter)
</label>
<input
type="text"
value={barcodeInput}
onChange={(e) => setBarcodeInput(e.target.value)}
className="w-full px-4 py-2 bg-slate-900 border border-white/20 rounded text-white font-mono focus:border-neon-blue focus:outline-none"
placeholder="Scan or enter barcode..."
/>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-white/10">
<button
onClick={onClose}
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"
>
Cancel
</button>
<button
onClick={handlePrint}
className="px-6 py-2 bg-neon-blue/20 hover:bg-neon-blue/30 border border-neon-blue text-neon-blue font-tech font-bold rounded transition-colors shadow-glow-blue"
>
<div className="flex items-center gap-2">
<Printer className="w-4 h-4" />
<span>PRINT</span>
</div>
</button>
</div>
</div>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen } from 'lucide-react';
import { VisionMenu } from '../VisionMenu';
import { FunctionMenu } from '../FunctionMenu';
import { ManualPrintDialog, PrintData } from '../ManualPrintDialog';
import { comms } from '../../communication';
import { useAlert } from '../../contexts/AlertContext';
@@ -18,6 +19,7 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
const location = useLocation();
const [showVisionMenu, setShowVisionMenu] = useState(false);
const [showFunctionMenu, setShowFunctionMenu] = useState(false);
const [showManualPrintDialog, setShowManualPrintDialog] = useState(false);
const { showAlert } = useAlert();
const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview;
@@ -47,10 +49,23 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
}
};
const handleManualPrint = async (printData: PrintData): Promise<{ success: boolean; message: string }> => {
try {
const result = await comms.executeManualPrint(printData);
return result;
} catch (error: any) {
return {
success: false,
message: error.message || 'Unknown error'
};
}
};
return (
<>
<VisionMenu isOpen={showVisionMenu} onClose={() => setShowVisionMenu(false)} />
<FunctionMenu isOpen={showFunctionMenu} onClose={() => setShowFunctionMenu(false)} />
<ManualPrintDialog isOpen={showManualPrintDialog} onClose={() => setShowManualPrintDialog(false)} onPrint={handleManualPrint} />
<header className="absolute top-0 left-0 right-0 h-20 px-6 flex items-center justify-between z-40 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
<div
className="flex items-center gap-4 pointer-events-auto cursor-pointer group"
@@ -97,7 +112,7 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
<span className="leading-tight">LIGHT</span>
</button>
<button
onClick={() => handleCommand(() => comms.openManualPrint(), 'Manual Print')}
onClick={() => setShowManualPrintDialog(true)}
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] text-slate-400 hover:text-cyan-400 hover:bg-white/5"
title="Open Manual Print"
>