import React, {useEffect, useState} from 'react' import axios from 'axios' import {Head, usePage} from '@inertiajs/react' import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' import { faArrowLeft, faQuestionCircle, faBoxOpen, faClipboardList, faCubes, faPlus, faBarcode, } from '@fortawesome/free-solid-svg-icons' import {toast, Toaster} from 'react-hot-toast' import Tile from '../Components/Tile' import TileLayout from '../Components/TileLayout' import ModalPDA from '../Components/ModalPDA' // your extracted modal forms: import SetStockModal from '../Components/modals/SetStockModal' import OtherReplacementModal from '../Components/modals/OtherReplacementModal' import CountStockModal from '../Components/modals/CountStockModal' import {Batch} from "@/interfaces/interfaces"; import {StockBatch, StockPosition, StockSection} from "@/types"; import BatchInfoModal from "@/Components/modals/BatchInfoModal"; import {downloadBatchBarcode} from "@/functions/functions" import EditStockSections from "@/Components/modals/EditStockSections"; import MoveSectionModal from "@/Components/modals/MoveSectionModal"; import ChangeCountModal from "@/Components/modals/ChangeCountModal"; import error_scanner_sound from "@/sounds/error_scanner.mp3"; import SetStockModalFromTemp from "@/Components/modals/SetStockModalFromTemp"; type Role = 'Expedice' | 'Skladnik' // actions per role const roleActions: Record = { Expedice: ['stockSectionScanned', 'labelScanned'], Skladnik: ['batchScan', 'stockScan'], } // configuration for each tile: either opens a modal (modalKey), // or performs an API call (onClick) type TileConfig = { title: string icon: any modalKey?: ModalKey onClick?: () => void } type ModalKey = 'setStock' | 'setStockFromTemp' | 'otherReplacement' | 'countStock' | 'batchInfo' | 'editStock' | 'moveStock' | 'changeCount' | null type PdaViewProps = { closeParent: () => void }; export default function PdaView({closeParent}: PdaViewProps) { const tilesConfig: Record> = { Expedice: { stockSectionScanned: [ {title: 'Not Present', icon: faBoxOpen, onClick: () => toast('Not Present clicked')}, {title: 'Na pozici je jiny ovladac', icon: faBoxOpen, onClick: () => toast('Not Present clicked')}, { title: 'Present but Shouldn’t', icon: faClipboardList, onClick: async () => { // example direct axios call try { await axios.post('/api/presence-error', {}, {withCredentials: true}) toast.success('Reported!') } catch { toast.error('Failed to report') } }, }, {title: 'Other Replacement (na test/one time)', icon: faPlus, modalKey: 'otherReplacement'}, {title: 'Pridej vazbu na jiny ovladac', icon: faPlus, onClick: () => toast('Batch Info clicked')}, { title: 'Pridej docasnou vazbu (nez prijde jine zbozi, i casove omezene, nejnizsi priorita)', icon: faPlus, onClick: () => toast('Batch Info clicked') }, {title: 'Doplnit zbozi / dej vic kusu', icon: faPlus, onClick: () => toast('Batch Info clicked')}, { title: 'Report - chci zmenit pozici(bliz / dal od expedice)', icon: faPlus, onClick: () => toast('Batch Info clicked') }, { title: 'Pultovy prodej', icon: faPlus, onClick: () => toast('- naskenuju na webu / naskenuju na skladovem miste, obj. se udela automaticky, zakaznik si muze v rohu na PC vyplnit osobni udaje na special formulari - pak customera prida obsluha do obj') }, ], labelScanned: [ {title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked')}, ] }, Skladnik: { batchScan: [ {title: 'Batch Info', icon: faClipboardList, modalKey: 'batchInfo'}, {title: 'Set Stock', icon: faCubes, modalKey: 'setStock'}, {title: 'Count Stock', icon: faPlus, modalKey: 'countStock'}, { title: 'Stitkovani (male stitky)', icon: faClipboardList, onClick: () => toast('Stitkovani (male stitky)') }, { title: 'Tisk QR kod na krabice', icon: faClipboardList, onClick: () => downloadBatchBarcode(selectedBatch?.id) }, ], stockScan: [ {title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked')}, {title: 'Set Stock', icon: faCubes, modalKey: 'setStockFromTemp'}, { title: 'Zmena skladoveho mista (i presun jen casti kusu)', icon: faClipboardList, modalKey: 'moveStock' }, {title: 'Uprava skladoveho mista (mene sekci, apod)', icon: faClipboardList, modalKey: 'editStock'}, {title: 'Zmena poctu', icon: faClipboardList, modalKey: 'changeCount'}, { title: 'Discard (odebrat ze skladoveho mista / posilame zpet)', icon: faClipboardList, onClick: () => toast('Batch Info clicked') }, ], others: [ {title: 'Nove skladove misto', icon: faClipboardList, onClick: () => toast('Batch Info clicked')}, ] }, } const { auth: {user}, } = usePage<{ auth: { user: { role: string } } }>().props const [role, setRole] = React.useState( user.role === 'admin' ? 'Expedice' : (user.role as Role) ) const [action, setAction] = React.useState('') const [activeModal, setActiveModal] = React.useState(null) const isAdmin = true // const isAdmin = user.role === 'admin' const tabs: Role[] = ['Expedice', 'Skladnik'] const [selectedSerial, setSelectedSerial] = useState(null); const selectedSectionRef = React.useRef(null) const selectedPositionRef = React.useRef(null) const closeModal = () => { setActiveModal(null); if(selectedBatch) { refetchData(); } } const refetchData = async () => { try { const res = await axios.get('/api/stockBatches/' + selectedBatch?.id); console.log(res.data); setSelectedBatch(res.data.batch); } catch { toast.error('Unable to reload batch data'); } } // const closeModal = () => { // setActiveModal(null) // setPrevPosition(null) // setPrevSection(null) // setSelectedPosition(null) // setSelectedSection(null) // } const wrapOnClick = (orig?: () => void) => { return () => { if (orig) orig(); // after the action, close the PDA modal: closeParent(); }; }; const [selectedBatch, setSelectedBatch] = React.useState(); const [selectedPosition, setSelectedPosition] = React.useState(); const [selectedSection, setSelectedSection] = React.useState(); const [prevPosition, setPrevPosition] = useState(null) const [prevSection, setPrevSection] = useState(null) useEffect(() => { // fetchBatches(); // fetchPosition(); initScanner(); }, []); const initScanner = () => { // run once on mount (async () => { // see if we already have permission for any ports const ports = await navigator.serial.getPorts(); console.log("ports", ports); if (ports.length > 0) { // pick the first (or filter by vendorId/productId) const port = ports[0]; setSelectedSerial(port); // now you can immediately open it: connectToScanner(port); } })(); }; const handlePortRequest = async () => { try { const port = await navigator.serial.requestPort(); // <-- user gesture! setSelectedSerial(port); await connectToScanner(port); } catch (err) { console.error("User cancelled or error:", err); } }; const connectToScanner = async (port) => { try { // Request a port and open a connection console.log(selectedSerial); await port.open({baudRate: 9600}); // setIsConnected(true); const reader = port.readable.getReader(); const decoder = new TextDecoder(); let buffer = ""; // Buffer to accumulate chunks while (true) { const {value, done} = await reader.read(); if (done) { // Close the stream reader.releaseLock(); break; } if (value) { // Decode and append to the buffer buffer += decoder.decode(value, {stream: true}); // Process messages split by newline or other delimiter let lines = buffer.split(/[\n\t]/); buffer = lines.pop(); // Keep the last incomplete chunk for (const line of lines) { const trimmedLine = line.trim(); sendToBackend(trimmedLine); } } } } catch (error) { console.error("Error connecting to scanner:", error); } }; const sendToBackend = async (scannedData: string) => { const audioError = document.getElementById("audio_error") as HTMLAudioElement; audioError.volume = 1.0; console.log("scannedData", scannedData); // 1) Parse JSON let payload: { barcode_type: string; payload: { id: number } }; try { payload = JSON.parse(scannedData); } catch (err) { toast.error("Invalid barcode data"); audioError.play(); return; } const {barcode_type, payload: inner} = payload; const {id} = inner; // 2) Call your API try { const response = await axios.post("/api/pda/barcodeScan", { barcode_type, payload: {id}, }); const {success, data, message, error} = response.data; console.log("response", data); if (!success) { // Laravel sends 200+ success=false for business errors, or 4xx/5xx for validation/not-found/etc toast.error( // Validation errors come back as an object of messages Array.isArray(message) ? Object.values(message).flat().join("; ") : message || "Scan failed" ); audioError.play(); return; } // 3) On success, data will be the model (batch/section/position/label) or undefined for carrier_scanned switch (barcode_type) { case "stock_batch": setAction("batchScan"); setSelectedBatch(data); // you now have the full batch record in `data` break; case "stock_section": setAction("stockScan"); // the “from” section is whatever was last in the ref const oldSection = selectedSectionRef.current // const oldPosition = selectedPositionRef.current // the “to” section is the data you just fetched const newSection = data setPrevSection(oldSection) // setPrevPosition(oldPosition) setSelectedSection(newSection) // setSelectedPosition(newSection.position) console.log("FROM (prevSection):", oldSection) console.log(" TO (selectedSection):", newSection) break case "stock_position": setPrevPosition(selectedPosition || null); setAction("stockScan"); setSelectedPosition(data); break; case "label_scanned": setAction("labelScan"); // perhaps `data.product` or `data.batch` are in here if you need them break; case "carrier_scanned": setAction("carrierScan"); // no data field returned, but success=true means it worked break; default: // validator already prevents this, but just in case toast.error(`Unknown barcode type: ${barcode_type}`); audioError.play(); } } catch (err: any) { // 4xx / 5xx HTTP errors, or network failures if (err.response) { // Laravel ValidationException → 422 if (err.response.status === 422) { const errors = err.response.data.message; toast.error( Array.isArray(errors) ? errors.join("; ") : typeof errors === "object" ? Object.values(errors).flat().join("; ") : errors ); } // ModelNotFound → 404 else if (err.response.status === 404) { toast.error(err.response.data.message || "Record not found"); } // Any other server error else { toast.error(err.response.data.message || "Server error"); } } else { // network / CORS / timeout toast.error("Network error"); } console.error("sendToBackend error:", err); audioError.play(); } }; useEffect(() => { selectedSectionRef.current = selectedSection ?? null }, [selectedSection]) useEffect(() => { selectedPositionRef.current = selectedPosition ?? null }, [selectedPosition]) return ( <> {/* Top bar */}
Back {!selectedSerial && ( )}
{/* Admin tabs */} {isAdmin && (
{tabs.map((r) => ( ))}
)} {/* Action selectors */}
{[...(roleActions[role] || []), 'clear'].map((act) => ( ))}
{/* Tiles */} {action && (() => { // grab the base array let tiles = tilesConfig[role][action] || [] // if we're stock-scanning but the position isn’t temporary, hide “Set Stock” if (action === 'stockScan' && !selectedPosition?.temporary) { tiles = tiles.filter(tile => tile.modalKey !== 'setStockFromTemp') } // render the filtered list return tiles.map(({ title, icon, onClick, modalKey }) => ( { if (modalKey) setActiveModal(modalKey) else if (onClick) onClick() }} /> )) })()} {/* Single Modal */} {activeModal === 'batchInfo' && } {activeModal === 'setStock' && } {activeModal === 'setStockFromTemp' && } {activeModal === 'otherReplacement' && } {activeModal === 'countStock' && } {activeModal === 'editStock' && } {activeModal === 'moveStock' && } {activeModal === 'changeCount' && } ) }