vat_wms/resources/js/Pages/PdaView.tsx

524 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Role, string[]> = {
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<Role, Record<string, TileConfig[]>> = {
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 Shouldnt',
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<Role>(
user.role === 'admin' ? 'Expedice' : (user.role as Role)
)
const [action, setAction] = React.useState<string>('')
const [activeModal, setActiveModal] = React.useState<ModalKey>(null)
const isAdmin = true
// const isAdmin = user.role === 'admin'
const tabs: Role[] = ['Expedice', 'Skladnik']
const [selectedSerial, setSelectedSerial] = useState(null);
const selectedSectionRef = React.useRef<StockSection | null>(null)
const selectedPositionRef = React.useRef<StockPosition | null>(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<StockBatch>();
const [selectedPosition, setSelectedPosition] = React.useState<StockPosition>();
const [selectedSection, setSelectedSection] = React.useState<StockSection>();
const [prevPosition, setPrevPosition] = useState<StockPosition | null>(null)
const [prevSection, setPrevSection] = useState<StockSection | null>(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 (
<>
<Head title="PDA View"/>
<audio id="audio_error" src={error_scanner_sound}>
</audio>
{/* Top bar */}
<div className="flex justify-between items-center bg-base-100 p-4 shadow">
<a className="link" href={route('dashboard')}><FontAwesomeIcon icon={faArrowLeft}/> Back</a>
{!selectedSerial && (
<button className="btn btn-ghost" onClick={handlePortRequest}>
<FontAwesomeIcon icon={faBarcode}/>
</button>
)}
<button className="btn btn-ghost">
<FontAwesomeIcon icon={faQuestionCircle}/> Help
</button>
</div>
{/* Admin tabs */}
{isAdmin && (
<div className="tabs justify-center bg-base-100">
{tabs.map((r) => (
<button
key={r}
className={`tab ${role === r ? 'tab-active' : ''}`}
onClick={() => {
setRole(r)
setAction('')
}}
>
{r}
</button>
))}
</div>
)}
{/* Action selectors */}
<div className="flex justify-center space-x-4 space-y-4 sm:space-y-0 p-4 sm:flex-nowrap flex-wrap">
{[...(roleActions[role] || []), 'clear'].map((act) => (
<button
key={act}
className="btn btn-outline"
onClick={() => {
const newAct = act === 'clear' ? '' : act
setAction(newAct)
toast(`Action set to ${newAct || 'none'}`)
}}
>
{act === 'clear' ? 'Clear Action' : act}
</button>
))}
</div>
{/* Tiles */}
<TileLayout>
{action && (() => {
// grab the base array
let tiles = tilesConfig[role][action] || []
// if we're stock-scanning but the position isnt 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 }) => (
<Tile
key={title}
title={title}
icon={icon}
onClick={() => {
if (modalKey) setActiveModal(modalKey)
else if (onClick) onClick()
}}
/>
))
})()}
</TileLayout>
{/* Single Modal */}
<ModalPDA isOpen={activeModal !== null} onClose={closeModal}>
{activeModal === 'batchInfo' && <BatchInfoModal onClose={closeModal} selectedBatch={selectedBatch}/>}
{activeModal === 'setStock' &&
<SetStockModal onClose={closeModal} selectedBatch={selectedBatch} selectedSection={selectedSection}
selectedPosition={selectedPosition}/>}
{activeModal === 'setStockFromTemp' &&
<SetStockModalFromTemp onClose={closeModal} selectedSection={selectedSection}
selectedPosition={selectedPosition}/>}
{activeModal === 'otherReplacement' && <OtherReplacementModal onClose={closeModal}/>}
{activeModal === 'countStock' && <CountStockModal onClose={closeModal} selectedBatch={selectedBatch}/>}
{activeModal === 'editStock' &&
<EditStockSections onClose={closeModal} selectedPosition={selectedPosition}/>}
{activeModal === 'moveStock' &&
<MoveSectionModal onClose={closeModal}
fromPosition={prevPosition}
fromSection={prevSection}
toPosition={selectedPosition}
toSection={selectedSection}/>
}
{activeModal === 'changeCount' &&
<ChangeCountModal onClose={closeModal} selectedPosition={selectedPosition}/>}
</ModalPDA>
<Toaster position="top-right"/>
</>
)
}