import React from 'react'; import superagent from 'superagent'; import { Segment, Header, Message, Button, Icon, Table, Modal, Label } from 'semantic-ui-react'; import * as ApiUtils from '../CloudApi/ApiUtils.js'; import BigShipperWrapper from './BigShipperWrapper.jsx'; import BigApiErrorMessage from '../CloudApi/BigApiErrorMessage.jsx'; import BigOrdersQueryEnhanced from '../BigOrders/BigOrdersQueryEnhanced.jsx'; import BigOrderTableCells from '../BigOrders/BigOrderTableCells.jsx'; import ApiButtonModal from '../CloudApi/ApiButtonModal.jsx'; export default class BigBatchShipper extends React.Component { constructor(props) { super(props); this.state = { loading: false, shipperResults: [], selectedOrders: new Set(), processingOrders: new Set(), completedOrders: new Set(), batchShippingInProgress: false, showResultsModal: false, batchShippingResults: [], showCancelModal: false, showErrorModal: false, batchError: null, processingLog: [], logExpanded: false, batchShippingMode: null, // null = not selected, 'labelsOnly' or 'generateAll' }; this.cancelRequested = false; this.printIframeRefs = {}; } async componentDidMount() { try { let schemas = await ApiUtils.getFabricatorSchemas(); this.setState({ schemas }); } catch (e) { console.log(e); } } addLogEntry(message, type = 'info') { const entry = { id: Date.now() + Math.random(), timestamp: new Date().toLocaleTimeString(), message, type }; this.setState(prevState => ({ processingLog: [...prevState.processingLog, entry] })); console.log(`[${entry.timestamp}] ${message}`); } clearLog() { this.setState({ processingLog: [] }); } async onUpdateQuery(params) { this.setState({ loading: true, shipperResults: [], selectedOrders: new Set() }); let shipperResults = []; try { const result = await superagent.get(`/api/admin/shipper/next3?${params.toString()}`); shipperResults = result.body; } catch (e) { console.log(e); } this.setState({ shipperResults, loading: false }); } toggleOrderSelection(bigOrderId) { const selectedOrders = new Set(this.state.selectedOrders); if (selectedOrders.has(bigOrderId)) { selectedOrders.delete(bigOrderId); } else { selectedOrders.add(bigOrderId); } this.setState({ selectedOrders }); } selectAllOrders() { const selectedOrders = new Set(); this.state.shipperResults.forEach(result => { selectedOrders.add(result.bigOrder.id); }); this.setState({ selectedOrders }); } deselectAllOrders() { this.setState({ selectedOrders: new Set() }); } printShippingLabel(shipmentId, labelUrl) { return new Promise((resolve, reject) => { this.addLogEntry(`Loading label for printing: ${labelUrl}`, 'info'); // Create a hidden iframe for printing const iframe = document.createElement('iframe'); iframe.style.position = 'absolute'; iframe.style.left = '-9999px'; iframe.style.width = '1px'; iframe.style.height = '1px'; iframe.src = `/file/${encodeURIComponent(labelUrl)}#toolbar=0&navpanes=0&scrollbar=0`; // Store reference this.printIframeRefs[shipmentId] = iframe; // Set a timeout in case the iframe fails to load const loadTimeout = setTimeout(() => { this.addLogEntry(`Label load timeout for shipment ${shipmentId}`, 'warning'); resolve(); // Continue anyway to not block batch processing }, 10000); // Handle load errors iframe.onerror = (e) => { clearTimeout(loadTimeout); this.addLogEntry(`Failed to load label for shipment ${shipmentId}: ${e}`, 'error'); resolve(); // Continue anyway }; // When the iframe loads, print it iframe.onload = () => { clearTimeout(loadTimeout); this.addLogEntry(`Label loaded, sending to printer (shipment ${shipmentId})...`, 'info'); try { iframe.focus(); iframe.contentWindow.print(); this.addLogEntry(`Print command sent for shipment ${shipmentId}`, 'success'); } catch (e) { this.addLogEntry(`Error sending print command: ${e.message}`, 'error'); console.error('Error printing label:', e); } // In kiosk mode with silent printing, print() returns immediately // Give a short delay to ensure the print job is queued setTimeout(() => { resolve(); }, 500); }; // Add to document document.body.appendChild(iframe); }); } cleanupPrintIframes() { // Remove all print iframes Object.values(this.printIframeRefs).forEach(iframe => { if (iframe && iframe.parentNode) { iframe.parentNode.removeChild(iframe); } }); this.printIframeRefs = {}; } async onCreateShipment(bigOrderId, shipmentGroupId) { this.setState({ loading: true }); try { const res = await superagent.post(`/api/admin/shipper/shipment`).send({ bigOrderId, shipmentGroupId }); window.location.href = `/shipper/shipment/${res.body.id}`; } catch (e) { console.log(e); this.setState({ loading: false }); } } async processSingleOrder(bigOrderId) { try { // Find the shipment for this order const shipperResult = this.state.shipperResults.find(r => r.bigOrder.id === bigOrderId); if (!shipperResult) { throw new Error('Shipment not found for order'); } const shipmentGroupId = shipperResult.shipmentGroupId; this.addLogEntry(`Processing order ${bigOrderId} with shipment group ${shipmentGroupId}`, 'info'); // Step 1: Create shipment this.addLogEntry(`Creating shipment for order ${bigOrderId}...`, 'info'); const createRes = await superagent.post(`/api/admin/shipper/shipment`).send({ bigOrderId, shipmentGroupId }); const shipmentId = createRes.body.id; this.addLogEntry(`Created shipment ${shipmentId}`, 'success'); // Get shipment details let shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); let bigShipment = shipmentRes.body.bigShipment; this.addLogEntry(`Shipment status: ${bigShipment.status}, ${bigShipment.inventorySlots.length} inventory slot(s)`, 'info'); // Step 2: Picking - automatically pick all items if (bigShipment.status === "Picking") { this.addLogEntry(`Starting picking phase...`, 'info'); // Log all inventory slots for (const slot of bigShipment.inventorySlots) { this.addLogEntry(` Slot ${slot.id}: ${slot.productType} - status: ${slot.status}`, 'info'); } // Pick all inventory slots that need to be picked for (const inventorySlot of bigShipment.inventorySlots) { if (inventorySlot.status !== "InventoryItemAdded") { this.addLogEntry(` Picking ${inventorySlot.productType} (slot ${inventorySlot.id})...`, 'info'); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "PickInventoryItem", inventorySlotId: inventorySlot.id, }); this.addLogEntry(` Picked ${inventorySlot.productType} successfully`, 'success'); } else { this.addLogEntry(` Slot ${inventorySlot.id} (${inventorySlot.productType}) already picked`, 'info'); } } // Refresh shipment to get updated inventory slot statuses after picking this.addLogEntry(`Refreshing shipment status after picking...`, 'info'); shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); bigShipment = shipmentRes.body.bigShipment; // Log updated slot statuses for (const slot of bigShipment.inventorySlots) { this.addLogEntry(` Slot ${slot.id}: ${slot.productType} - status: ${slot.status}`, 'info'); } // Verify all items have been picked before finishing const unpickedItems = bigShipment.inventorySlots.filter( slot => slot.status !== "InventoryItemAdded" ); if (unpickedItems.length > 0) { const unpickedTypes = unpickedItems.map(slot => `${slot.productType} (${slot.status})`).join(", "); this.addLogEntry(`ERROR: ${unpickedItems.length} item(s) not picked: ${unpickedTypes}`, 'error'); throw new Error(`Cannot finish picking: ${unpickedItems.length} item(s) not picked (${unpickedTypes})`); } // Finish picking this.addLogEntry(`All items picked, finishing picking phase...`, 'info'); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "FinishPicking" }); this.addLogEntry(`Picking phase completed`, 'success'); // Refresh shipment status shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); bigShipment = shipmentRes.body.bigShipment; this.addLogEntry(`Shipment status: ${bigShipment.status}`, 'info'); } // Step 3: Generate and print shipping label if (bigShipment.status === "PrepareShippingLabel") { this.addLogEntry(`Generating shipping label...`, 'info'); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "GenerateShippingLabel" }); // Refresh shipment status to get the label shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); bigShipment = shipmentRes.body.bigShipment; // Print the shipping label if (bigShipment.currentShippingLabel) { const labelUrl = bigShipment.currentShippingLabel.upload_label_url || bigShipment.currentShippingLabel.uploadLabelUrl; if (labelUrl) { this.addLogEntry(`Printing shipping label...`, 'info'); await this.printShippingLabel(shipmentId, labelUrl); this.addLogEntry(`Label sent to printer`, 'success'); } } } // Step 4: Packing if (bigShipment.status === "PrepareShippingLabel") { this.addLogEntry(`Starting packing phase...`, 'info'); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "StartPacking" }); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "FinishPacking" }); this.addLogEntry(`Packing phase completed`, 'success'); // Refresh shipment status shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); bigShipment = shipmentRes.body.bigShipment; } // Step 5: Complete shipment (only if generateAll mode) if (this.state.batchShippingMode === 'generateAll') { if (bigShipment.status === "PreparingToShip") { this.addLogEntry(`Completing shipment...`, 'info'); await superagent.put(`/api/admin/shipper/shipment/${shipmentId}`) .send({ action: "ShipIt" }); this.addLogEntry(`Shipment completed`, 'success'); // Refresh shipment status shipmentRes = await superagent.get(`/api/admin/shipper/shipment/${shipmentId}`).accept('json'); bigShipment = shipmentRes.body.bigShipment; } } else if (this.state.batchShippingMode === 'labelsOnly' && bigShipment.status === "PreparingToShip") { this.addLogEntry(`Label generated - skipping fulfillment (labels only mode)`, 'info'); } this.addLogEntry(`Order ${bigOrderId} completed successfully (shipment ${shipmentId})`, 'success'); return { bigOrderId, shipmentId, success: true, message: `Shipment ${shipmentId} created and processed successfully` }; } catch (error) { this.addLogEntry(`ERROR processing order ${bigOrderId}: ${error.message || error}`, 'error'); console.error('Error processing order:', bigOrderId, error); // Re-throw the error so it can be caught by onBatchShip throw { bigOrderId, originalError: error, message: error.message || error.toString(), status: error.status, response: error.response }; } } async onBatchShip() { this.cancelRequested = false; this.clearLog(); this.setState({ batchShippingInProgress: true, batchShippingResults: [], batchError: null }); const selectedOrderIds = Array.from(this.state.selectedOrders); const results = []; this.addLogEntry(`Starting batch shipping for ${selectedOrderIds.length} order(s)`, 'info'); try { // Process each order in series for (const bigOrderId of selectedOrderIds) { // Check if cancellation was requested if (this.cancelRequested) { results.push({ bigOrderId, success: false, message: 'Cancelled by user' }); continue; } // Mark order as processing this.setState(prevState => ({ processingOrders: new Set(prevState.processingOrders).add(bigOrderId) })); try { const result = await this.processSingleOrder(bigOrderId); results.push(result); // Mark order as completed this.setState(prevState => { const newProcessing = new Set(prevState.processingOrders); newProcessing.delete(bigOrderId); const newCompleted = new Set(prevState.completedOrders); newCompleted.add(bigOrderId); const newSelected = new Set(prevState.selectedOrders); newSelected.delete(bigOrderId); return { processingOrders: newProcessing, completedOrders: newCompleted, selectedOrders: newSelected }; }); } catch (error) { // Stop processing immediately on error console.error('Error processing order, stopping batch:', bigOrderId, error); // Mark current order as failed and remove from processing this.setState(prevState => { const newProcessing = new Set(prevState.processingOrders); newProcessing.delete(bigOrderId); return { processingOrders: newProcessing, batchShippingInProgress: false, batchError: error, showErrorModal: true }; }); // Cleanup and stop this.cancelRequested = false; this.cleanupPrintIframes(); return; // Exit the function immediately } } // All orders processed successfully - show results modal this.setState({ batchShippingInProgress: false, batchShippingResults: results, showResultsModal: true }); // Cleanup print iframes after a delay (to ensure printing is complete) setTimeout(() => { this.cleanupPrintIframes(); }, 5000); } catch (unexpectedError) { // Handle any unexpected errors console.error('Unexpected error in batch processing:', unexpectedError); this.setState({ batchShippingInProgress: false, batchError: unexpectedError, showErrorModal: true }); this.cleanupPrintIframes(); } this.cancelRequested = false; } closeResultsModal() { this.setState({ showResultsModal: false }); } closeErrorModal() { this.setState({ showErrorModal: false, batchError: null }); } onRequestCancel() { this.setState({ showCancelModal: true }); } closeCancelModal() { this.setState({ showCancelModal: false }); } onConfirmCancel() { this.cancelRequested = true; this.setState({ showCancelModal: false }); } render() { if (!this.state.schemas) return null; const allSelected = this.state.shipperResults.length > 0 && this.state.shipperResults.every(result => this.state.selectedOrders.has(result.bigOrder.id)); let bigShipperResults = null; if (this.state.shipperResults && this.state.shipperResults.length > 0) { bigShipperResults = this.state.shipperResults.map((result) => { const isSelected = this.state.selectedOrders.has(result.bigOrder.id); const isProcessing = this.state.processingOrders.has(result.bigOrder.id); const isCompleted = this.state.completedOrders.has(result.bigOrder.id); const isDisabled = isProcessing || isCompleted || this.state.batchShippingInProgress; return ( { if (!isDisabled) { e.stopPropagation(); this.toggleOrderSelection(result.bigOrder.id); } }} positive={isSelected && !isCompleted} active={isCompleted} > e.stopPropagation()}> {isProcessing && ( )} {isCompleted && ( )} {!isProcessing && !isCompleted && ( this.toggleOrderSelection(result.bigOrder.id)} disabled={this.state.batchShippingInProgress} style={{ cursor: this.state.batchShippingInProgress ? 'not-allowed' : 'pointer' }} /> )} ); }); } else { bigShipperResults = ( No big orders to ship

There are no big orders to ship with this selection!

); } const successCount = this.state.batchShippingResults.filter(r => r.success).length; const failureCount = this.state.batchShippingResults.filter(r => !r.success).length; return (
An error occurred during batch processing

Processing has been stopped to prevent further issues.

{this.state.batchError && (
Error Details
Order ID: {this.state.batchError.bigOrderId} Error Message: {this.state.batchError.message} {this.state.batchError.status && ( HTTP Status: {this.state.batchError.status} )} {this.state.batchError.response && this.state.batchError.response.body && ( Server Response:
                                                        {JSON.stringify(this.state.batchError.response.body, null, 2)}
                                                    
)}
)}
Are you sure you want to cancel?

This will stop processing any remaining orders. Orders that have already been processed will remain completed.

Batch Shipping Complete!

Processed {this.state.batchShippingResults.length} orders

{successCount > 0 && ( {successCount} order{successCount !== 1 ? 's' : ''} processed successfully )} {failureCount > 0 && ( {failureCount} order{failureCount !== 1 ? 's' : ''} failed )}
{this.state.batchShippingResults.length > 0 && ( Order ID Status Message {this.state.batchShippingResults.map((result) => ( {result.bigOrderId} {result.success ? ( ) : ( )} {result.message} ))}
)}
{this.state.batchShippingInProgress && (
Query disabled during batch processing
)}
{this.state.loading &&
Looking for orders to ship...
} {this.state.shipperResults && this.state.shipperResults.length > 0 && ( <>
📦 Showing {this.state.shipperResults.length} Orders Ready To Ship {this.state.selectedOrders.size > 0 && ` • ${this.state.selectedOrders.size} Selected`} {this.state.batchShippingInProgress && ( {' • '} Processing... )}
this.setState({ batchShippingMode: null })} confirmDisabled={this.state.batchShippingMode === null} closeImmediately={true} >

You are about to start the batch shipping process for {this.state.selectedOrders.size} selected orders.

This will create shipments for all selected orders that are ready to ship.

Important

Please review the selected orders before proceeding. This action will start the shipping process for all selected orders.

{this.state.batchShippingInProgress && ( )} {this.state.logExpanded && (
{ if (el) el.scrollTop = el.scrollHeight; }} > {this.state.processingLog.map((entry) => (
[{entry.timestamp}]{' '} {entry.type === 'error' && } {entry.type === 'warning' && } {entry.type === 'success' && } {entry.message}
))}
)}
); })()} {!this.state.loading && ( Order # / Priority Created Updated Customer Products IPD Status {bigShipperResults}
)} ); } }