FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

JobPickExecution.tsx 54 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514
  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Stack,
  6. TextField,
  7. Typography,
  8. Alert,
  9. CircularProgress,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. Checkbox,
  18. TablePagination,
  19. Modal,
  20. } from "@mui/material";
  21. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  22. import { useTranslation } from "react-i18next";
  23. import { useRouter } from "next/navigation";
  24. import {
  25. updateStockOutLineStatus,
  26. createStockOutLine,
  27. recordPickExecutionIssue,
  28. fetchFGPickOrders,
  29. FGPickOrderResponse,
  30. autoAssignAndReleasePickOrder,
  31. AutoAssignReleaseResponse,
  32. checkPickOrderCompletion,
  33. PickOrderCompletionResponse,
  34. checkAndCompletePickOrderByConsoCode
  35. } from "@/app/api/pickOrder/actions";
  36. // ✅ 修改:使用 Job Order API
  37. import {
  38. fetchJobOrderLotsHierarchical,
  39. fetchUnassignedJobOrderPickOrders,
  40. assignJobOrderPickOrder
  41. } from "@/app/api/jo/actions";
  42. import { fetchNameList, NameList } from "@/app/api/user/actions";
  43. import {
  44. FormProvider,
  45. useForm,
  46. } from "react-hook-form";
  47. import SearchBox, { Criterion } from "../SearchBox";
  48. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  49. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  50. import QrCodeIcon from '@mui/icons-material/QrCode';
  51. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  52. import { useSession } from "next-auth/react";
  53. import { SessionWithTokens } from "@/config/authConfig";
  54. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  55. import GoodPickExecutionForm from "./JobPickExecutionForm";
  56. import FGPickOrderCard from "./FGPickOrderCard";
  57. interface Props {
  58. filterArgs: Record<string, any>;
  59. }
  60. // ✅ QR Code Modal Component (from GoodPickExecution)
  61. const QrCodeModal: React.FC<{
  62. open: boolean;
  63. onClose: () => void;
  64. lot: any | null;
  65. onQrCodeSubmit: (lotNo: string) => void;
  66. combinedLotData: any[];
  67. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  68. const { t } = useTranslation("jo");
  69. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  70. const [manualInput, setManualInput] = useState<string>('');
  71. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  72. const [manualInputError, setManualInputError] = useState<boolean>(false);
  73. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  74. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  75. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  76. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  77. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  78. // Process scanned QR codes
  79. useEffect(() => {
  80. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  81. const latestQr = qrValues[qrValues.length - 1];
  82. if (processedQrCodes.has(latestQr)) {
  83. console.log("QR code already processed, skipping...");
  84. return;
  85. }
  86. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  87. try {
  88. const qrData = JSON.parse(latestQr);
  89. if (qrData.stockInLineId && qrData.itemId) {
  90. setIsProcessingQr(true);
  91. setQrScanFailed(false);
  92. fetchStockInLineInfo(qrData.stockInLineId)
  93. .then((stockInLineInfo) => {
  94. console.log("Stock in line info:", stockInLineInfo);
  95. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  96. if (stockInLineInfo.lotNo === lot.lotNo) {
  97. console.log(`✅ QR Code verified for lot: ${lot.lotNo}`);
  98. setQrScanSuccess(true);
  99. onQrCodeSubmit(lot.lotNo);
  100. onClose();
  101. resetScan();
  102. } else {
  103. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  104. setQrScanFailed(true);
  105. setManualInputError(true);
  106. setManualInputSubmitted(true);
  107. }
  108. })
  109. .catch((error) => {
  110. console.error("Error fetching stock in line info:", error);
  111. setScannedQrResult('Error fetching data');
  112. setQrScanFailed(true);
  113. setManualInputError(true);
  114. setManualInputSubmitted(true);
  115. })
  116. .finally(() => {
  117. setIsProcessingQr(false);
  118. });
  119. } else {
  120. const qrContent = latestQr.replace(/[{}]/g, '');
  121. setScannedQrResult(qrContent);
  122. if (qrContent === lot.lotNo) {
  123. setQrScanSuccess(true);
  124. onQrCodeSubmit(lot.lotNo);
  125. onClose();
  126. resetScan();
  127. } else {
  128. setQrScanFailed(true);
  129. setManualInputError(true);
  130. setManualInputSubmitted(true);
  131. }
  132. }
  133. } catch (error) {
  134. console.log("QR code is not JSON format, trying direct comparison");
  135. const qrContent = latestQr.replace(/[{}]/g, '');
  136. setScannedQrResult(qrContent);
  137. if (qrContent === lot.lotNo) {
  138. setQrScanSuccess(true);
  139. onQrCodeSubmit(lot.lotNo);
  140. onClose();
  141. resetScan();
  142. } else {
  143. setQrScanFailed(true);
  144. setManualInputError(true);
  145. setManualInputSubmitted(true);
  146. }
  147. }
  148. }
  149. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  150. // Clear states when modal opens
  151. useEffect(() => {
  152. if (open) {
  153. setManualInput('');
  154. setManualInputSubmitted(false);
  155. setManualInputError(false);
  156. setIsProcessingQr(false);
  157. setQrScanFailed(false);
  158. setQrScanSuccess(false);
  159. setScannedQrResult('');
  160. setProcessedQrCodes(new Set());
  161. }
  162. }, [open]);
  163. useEffect(() => {
  164. if (lot) {
  165. setManualInput('');
  166. setManualInputSubmitted(false);
  167. setManualInputError(false);
  168. setIsProcessingQr(false);
  169. setQrScanFailed(false);
  170. setQrScanSuccess(false);
  171. setScannedQrResult('');
  172. setProcessedQrCodes(new Set());
  173. }
  174. }, [lot]);
  175. // Auto-submit manual input when it matches
  176. useEffect(() => {
  177. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  178. console.log(' Auto-submitting manual input:', manualInput.trim());
  179. const timer = setTimeout(() => {
  180. setQrScanSuccess(true);
  181. onQrCodeSubmit(lot.lotNo);
  182. onClose();
  183. setManualInput('');
  184. setManualInputError(false);
  185. setManualInputSubmitted(false);
  186. }, 200);
  187. return () => clearTimeout(timer);
  188. }
  189. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  190. const handleManualSubmit = () => {
  191. if (manualInput.trim() === lot?.lotNo) {
  192. setQrScanSuccess(true);
  193. onQrCodeSubmit(lot.lotNo);
  194. onClose();
  195. setManualInput('');
  196. } else {
  197. setQrScanFailed(true);
  198. setManualInputError(true);
  199. setManualInputSubmitted(true);
  200. }
  201. };
  202. useEffect(() => {
  203. if (open) {
  204. startScan();
  205. }
  206. }, [open, startScan]);
  207. return (
  208. <Modal open={open} onClose={onClose}>
  209. <Box sx={{
  210. position: 'absolute',
  211. top: '50%',
  212. left: '50%',
  213. transform: 'translate(-50%, -50%)',
  214. bgcolor: 'background.paper',
  215. p: 3,
  216. borderRadius: 2,
  217. minWidth: 400,
  218. }}>
  219. <Typography variant="h6" gutterBottom>
  220. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  221. </Typography>
  222. {isProcessingQr && (
  223. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  224. <Typography variant="body2" color="primary">
  225. {t("Processing QR code...")}
  226. </Typography>
  227. </Box>
  228. )}
  229. <Box sx={{ mb: 2 }}>
  230. <Typography variant="body2" gutterBottom>
  231. <strong>{t("Manual Input")}:</strong>
  232. </Typography>
  233. <TextField
  234. fullWidth
  235. size="small"
  236. value={manualInput}
  237. onChange={(e) => {
  238. setManualInput(e.target.value);
  239. if (qrScanFailed || manualInputError) {
  240. setQrScanFailed(false);
  241. setManualInputError(false);
  242. setManualInputSubmitted(false);
  243. }
  244. }}
  245. sx={{ mb: 1 }}
  246. error={manualInputSubmitted && manualInputError}
  247. helperText={
  248. manualInputSubmitted && manualInputError
  249. ? `${t("The input is not the same as the expected lot number.")}`
  250. : ''
  251. }
  252. />
  253. <Button
  254. variant="contained"
  255. onClick={handleManualSubmit}
  256. disabled={!manualInput.trim()}
  257. size="small"
  258. color="primary"
  259. >
  260. {t("Submit")}
  261. </Button>
  262. </Box>
  263. {qrValues.length > 0 && (
  264. <Box sx={{
  265. mb: 2,
  266. p: 2,
  267. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  268. borderRadius: 1
  269. }}>
  270. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  271. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  272. </Typography>
  273. {qrScanSuccess && (
  274. <Typography variant="caption" color="success" display="block">
  275. ✅ {t("Verified successfully!")}
  276. </Typography>
  277. )}
  278. </Box>
  279. )}
  280. <Box sx={{ mt: 2, textAlign: 'right' }}>
  281. <Button onClick={onClose} variant="outlined">
  282. {t("Cancel")}
  283. </Button>
  284. </Box>
  285. </Box>
  286. </Modal>
  287. );
  288. };
  289. const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
  290. const { t } = useTranslation("jo");
  291. const router = useRouter();
  292. const { data: session } = useSession() as { data: SessionWithTokens | null };
  293. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  294. // ✅ 修改:使用 Job Order 数据结构
  295. const [jobOrderData, setJobOrderData] = useState<any>(null);
  296. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  297. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  298. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  299. // ✅ 添加未分配订单状态
  300. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  301. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  302. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  303. const [qrScanInput, setQrScanInput] = useState<string>('');
  304. const [qrScanError, setQrScanError] = useState<boolean>(false);
  305. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  306. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  307. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  308. const [paginationController, setPaginationController] = useState({
  309. pageNum: 0,
  310. pageSize: 10,
  311. });
  312. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  313. const initializationRef = useRef(false);
  314. const autoAssignRef = useRef(false);
  315. const formProps = useForm();
  316. const errors = formProps.formState.errors;
  317. // ✅ Add QR modal states
  318. const [qrModalOpen, setQrModalOpen] = useState(false);
  319. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  320. // ✅ Add GoodPickExecutionForm states
  321. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  322. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  323. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  324. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  325. // ✅ Add these missing state variables
  326. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  327. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  328. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  329. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  330. // ✅ 修改:加载未分配的 Job Order 订单
  331. const loadUnassignedOrders = useCallback(async () => {
  332. setIsLoadingUnassigned(true);
  333. try {
  334. const orders = await fetchUnassignedJobOrderPickOrders();
  335. setUnassignedOrders(orders);
  336. } catch (error) {
  337. console.error("Error loading unassigned orders:", error);
  338. } finally {
  339. setIsLoadingUnassigned(false);
  340. }
  341. }, []);
  342. // ✅ 修改:分配订单给当前用户
  343. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  344. if (!currentUserId) {
  345. console.error("Missing user id in session");
  346. return;
  347. }
  348. try {
  349. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  350. if (result.message === "Successfully assigned") {
  351. console.log("✅ Successfully assigned pick order");
  352. // 刷新数据
  353. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  354. // 重新加载未分配订单列表
  355. loadUnassignedOrders();
  356. } else {
  357. console.warn("⚠️ Assignment failed:", result.message);
  358. alert(`Assignment failed: ${result.message}`);
  359. }
  360. } catch (error) {
  361. console.error("❌ Error assigning order:", error);
  362. alert("Error occurred during assignment");
  363. }
  364. }, [currentUserId, loadUnassignedOrders]);
  365. const fetchFgPickOrdersData = useCallback(async () => {
  366. if (!currentUserId) return;
  367. setFgPickOrdersLoading(true);
  368. try {
  369. // Get all pick order IDs from combinedLotData
  370. const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
  371. if (pickOrderIds.length === 0) {
  372. setFgPickOrders([]);
  373. return;
  374. }
  375. // Fetch FG pick orders for each pick order ID
  376. const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
  377. fetchFGPickOrders(pickOrderId)
  378. );
  379. const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
  380. // Flatten the results (each fetchFGPickOrders returns an array)
  381. const allFgPickOrders = fgPickOrdersResults.flat();
  382. setFgPickOrders(allFgPickOrders);
  383. console.log("✅ Fetched FG pick orders:", allFgPickOrders);
  384. } catch (error) {
  385. console.error("❌ Error fetching FG pick orders:", error);
  386. setFgPickOrders([]);
  387. } finally {
  388. setFgPickOrdersLoading(false);
  389. }
  390. }, [currentUserId, combinedLotData]);
  391. useEffect(() => {
  392. if (combinedLotData.length > 0) {
  393. fetchFgPickOrdersData();
  394. }
  395. }, [combinedLotData, fetchFgPickOrdersData]);
  396. // ✅ Handle QR code button click
  397. const handleQrCodeClick = (pickOrderId: number) => {
  398. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  399. // TODO: Implement QR code functionality
  400. };
  401. // ✅ 修改:使用 Job Order API 获取数据
  402. const fetchJobOrderData = useCallback(async (userId?: number) => {
  403. setCombinedDataLoading(true);
  404. try {
  405. const userIdToUse = userId || currentUserId;
  406. console.log(" fetchJobOrderData called with userId:", userIdToUse);
  407. if (!userIdToUse) {
  408. console.warn("⚠️ No userId available, skipping API call");
  409. setJobOrderData(null);
  410. setCombinedLotData([]);
  411. setOriginalCombinedData([]);
  412. return;
  413. }
  414. // ✅ 使用 Job Order API
  415. const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
  416. console.log("✅ Job Order data:", jobOrderData);
  417. setJobOrderData(jobOrderData);
  418. // ✅ Transform hierarchical data to flat structure for the table
  419. const flatLotData: any[] = [];
  420. if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
  421. jobOrderData.pickOrderLines.forEach((line: any) => {
  422. if (line.lots && line.lots.length > 0) {
  423. line.lots.forEach((lot: any) => {
  424. flatLotData.push({
  425. // Pick order info
  426. pickOrderId: jobOrderData.pickOrder.id,
  427. pickOrderCode: jobOrderData.pickOrder.code,
  428. pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
  429. pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
  430. pickOrderType: jobOrderData.pickOrder.type,
  431. pickOrderStatus: jobOrderData.pickOrder.status,
  432. pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
  433. // Pick order line info
  434. pickOrderLineId: line.id,
  435. pickOrderLineRequiredQty: line.requiredQty,
  436. pickOrderLineStatus: line.status,
  437. // Item info
  438. itemId: line.itemId,
  439. itemCode: line.itemCode,
  440. itemName: line.itemName,
  441. uomCode: line.uomCode,
  442. uomDesc: line.uomDesc,
  443. // Lot info
  444. lotId: lot.lotId,
  445. lotNo: lot.lotNo,
  446. expiryDate: lot.expiryDate,
  447. location: lot.location,
  448. availableQty: lot.availableQty,
  449. requiredQty: lot.requiredQty,
  450. actualPickQty: lot.actualPickQty,
  451. lotStatus: lot.lotStatus,
  452. lotAvailability: lot.lotAvailability,
  453. processingStatus: lot.processingStatus,
  454. stockOutLineId: lot.stockOutLineId,
  455. stockOutLineStatus: lot.stockOutLineStatus,
  456. stockOutLineQty: lot.stockOutLineQty,
  457. // Router info
  458. routerIndex: lot.routerIndex,
  459. secondQrScanStatus: lot.secondQrScanStatus,
  460. routerArea: lot.routerArea,
  461. routerRoute: lot.routerRoute,
  462. uomShortDesc: lot.uomShortDesc
  463. });
  464. });
  465. }
  466. });
  467. }
  468. console.log("✅ Transformed flat lot data:", flatLotData);
  469. setCombinedLotData(flatLotData);
  470. setOriginalCombinedData(flatLotData);
  471. // ✅ 计算完成状态并发送事件
  472. const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
  473. lot.processingStatus === 'completed'
  474. );
  475. // ✅ 发送完成状态事件,包含标签页信息
  476. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  477. detail: {
  478. allLotsCompleted: allCompleted,
  479. tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件
  480. }
  481. }));
  482. } catch (error) {
  483. console.error("❌ Error fetching job order data:", error);
  484. setJobOrderData(null);
  485. setCombinedLotData([]);
  486. setOriginalCombinedData([]);
  487. // ✅ 如果加载失败,禁用打印按钮
  488. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  489. detail: {
  490. allLotsCompleted: false,
  491. tabIndex: 0
  492. }
  493. }));
  494. } finally {
  495. setCombinedDataLoading(false);
  496. }
  497. }, [currentUserId]);
  498. // ✅ 修改:初始化时加载数据
  499. useEffect(() => {
  500. if (session && currentUserId && !initializationRef.current) {
  501. console.log("✅ Session loaded, initializing job order...");
  502. initializationRef.current = true;
  503. // 加载 Job Order 数据
  504. fetchJobOrderData();
  505. // 加载未分配订单
  506. loadUnassignedOrders();
  507. }
  508. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
  509. // ✅ Add event listener for manual assignment
  510. useEffect(() => {
  511. const handlePickOrderAssigned = () => {
  512. console.log("🔄 Pick order assigned event received, refreshing data...");
  513. fetchJobOrderData();
  514. };
  515. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  516. return () => {
  517. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  518. };
  519. }, [fetchJobOrderData]);
  520. // ✅ Handle QR code submission for matched lot (external scanning)
  521. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  522. console.log(`✅ Processing QR Code for lot: ${lotNo}`);
  523. // ✅ Use current data without refreshing to avoid infinite loop
  524. const currentLotData = combinedLotData;
  525. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  526. const matchingLots = currentLotData.filter(lot =>
  527. lot.lotNo === lotNo ||
  528. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  529. );
  530. if (matchingLots.length === 0) {
  531. console.error(`❌ Lot not found: ${lotNo}`);
  532. setQrScanError(true);
  533. setQrScanSuccess(false);
  534. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  535. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  536. return;
  537. }
  538. console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots);
  539. setQrScanError(false);
  540. try {
  541. let successCount = 0;
  542. let errorCount = 0;
  543. for (const matchingLot of matchingLots) {
  544. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  545. if (matchingLot.stockOutLineId) {
  546. const stockOutLineUpdate = await updateStockOutLineStatus({
  547. id: matchingLot.stockOutLineId,
  548. status: 'checked',
  549. qty: 0
  550. });
  551. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  552. // Treat multiple backend shapes as success (type-safe via any)
  553. const r: any = stockOutLineUpdate as any;
  554. const updateOk =
  555. r?.code === 'SUCCESS' ||
  556. typeof r?.id === 'number' ||
  557. r?.type === 'checked' ||
  558. r?.status === 'checked' ||
  559. typeof r?.entity?.id === 'number' ||
  560. r?.entity?.status === 'checked';
  561. if (updateOk) {
  562. successCount++;
  563. } else {
  564. errorCount++;
  565. }
  566. } else {
  567. const createStockOutLineData = {
  568. consoCode: matchingLot.pickOrderConsoCode,
  569. pickOrderLineId: matchingLot.pickOrderLineId,
  570. inventoryLotLineId: matchingLot.lotId,
  571. qty: 0
  572. };
  573. const createResult = await createStockOutLine(createStockOutLineData);
  574. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  575. if (createResult && createResult.code === "SUCCESS") {
  576. // Immediately set status to checked for new line
  577. let newSolId: number | undefined;
  578. const anyRes: any = createResult as any;
  579. if (typeof anyRes?.id === 'number') {
  580. newSolId = anyRes.id;
  581. } else if (anyRes?.entity) {
  582. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  583. }
  584. if (newSolId) {
  585. const setChecked = await updateStockOutLineStatus({
  586. id: newSolId,
  587. status: 'checked',
  588. qty: 0
  589. });
  590. if (setChecked && setChecked.code === "SUCCESS") {
  591. successCount++;
  592. } else {
  593. errorCount++;
  594. }
  595. } else {
  596. console.warn("Created stock out line but no ID returned; cannot set to checked");
  597. errorCount++;
  598. }
  599. } else {
  600. errorCount++;
  601. }
  602. }
  603. }
  604. // ✅ FIXED: Set refresh flag before refreshing data
  605. setIsRefreshingData(true);
  606. console.log("🔄 Refreshing data after QR code processing...");
  607. await fetchJobOrderData();
  608. if (successCount > 0) {
  609. console.log(`✅ QR Code processing completed: ${successCount} updated/created`);
  610. setQrScanSuccess(true);
  611. setQrScanError(false);
  612. setQrScanInput(''); // Clear input after successful processing
  613. setIsManualScanning(false);
  614. stopScan();
  615. resetScan();
  616. } else {
  617. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  618. setQrScanError(true);
  619. setQrScanSuccess(false);
  620. }
  621. } catch (error) {
  622. console.error("❌ Error processing QR code:", error);
  623. setQrScanError(true);
  624. setQrScanSuccess(false);
  625. // ✅ Still refresh data even on error
  626. setIsRefreshingData(true);
  627. await fetchJobOrderData();
  628. } finally {
  629. // ✅ Clear refresh flag after a short delay
  630. setTimeout(() => {
  631. setIsRefreshingData(false);
  632. }, 1000);
  633. }
  634. }, [combinedLotData, fetchJobOrderData]);
  635. const handleManualInputSubmit = useCallback(() => {
  636. if (qrScanInput.trim() !== '') {
  637. handleQrCodeSubmit(qrScanInput.trim());
  638. }
  639. }, [qrScanInput, handleQrCodeSubmit]);
  640. // ✅ Handle QR code submission from modal (internal scanning)
  641. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  642. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  643. console.log(`✅ QR Code verified for lot: ${lotNo}`);
  644. const requiredQty = selectedLotForQr.requiredQty;
  645. const lotId = selectedLotForQr.lotId;
  646. // Create stock out line
  647. const stockOutLineData: CreateStockOutLine = {
  648. consoCode: selectedLotForQr.pickOrderConsoCode,
  649. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  650. inventoryLotLineId: selectedLotForQr.lotId,
  651. qty: 0.0
  652. };
  653. try {
  654. await createStockOutLine(stockOutLineData);
  655. console.log("Stock out line created successfully!");
  656. // Close modal
  657. setQrModalOpen(false);
  658. setSelectedLotForQr(null);
  659. // Set pick quantity
  660. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  661. setTimeout(() => {
  662. setPickQtyData(prev => ({
  663. ...prev,
  664. [lotKey]: requiredQty
  665. }));
  666. console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  667. }, 500);
  668. // Refresh data
  669. await fetchJobOrderData();
  670. } catch (error) {
  671. console.error("Error creating stock out line:", error);
  672. }
  673. }
  674. }, [selectedLotForQr, fetchJobOrderData]);
  675. // ✅ Outside QR scanning - process QR codes from outside the page automatically
  676. useEffect(() => {
  677. if (qrValues.length > 0 && combinedLotData.length > 0) {
  678. const latestQr = qrValues[qrValues.length - 1];
  679. // Extract lot number from QR code
  680. let lotNo = '';
  681. try {
  682. const qrData = JSON.parse(latestQr);
  683. if (qrData.stockInLineId && qrData.itemId) {
  684. // For JSON QR codes, we need to fetch the lot number
  685. fetchStockInLineInfo(qrData.stockInLineId)
  686. .then((stockInLineInfo) => {
  687. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  688. const extractedLotNo = stockInLineInfo.lotNo;
  689. if (extractedLotNo) {
  690. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  691. handleQrCodeSubmit(extractedLotNo);
  692. }
  693. })
  694. .catch((error) => {
  695. console.error("Outside QR scan - Error fetching stock in line info:", error);
  696. });
  697. return; // Exit early for JSON QR codes
  698. }
  699. } catch (error) {
  700. // Not JSON format, treat as direct lot number
  701. lotNo = latestQr.replace(/[{}]/g, '');
  702. }
  703. // For direct lot number QR codes
  704. if (lotNo) {
  705. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  706. handleQrCodeSubmit(lotNo);
  707. }
  708. }
  709. }, [qrValues, combinedLotData, handleQrCodeSubmit]);
  710. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  711. if (value === '' || value === null || value === undefined) {
  712. setPickQtyData(prev => ({
  713. ...prev,
  714. [lotKey]: 0
  715. }));
  716. return;
  717. }
  718. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  719. if (isNaN(numericValue)) {
  720. setPickQtyData(prev => ({
  721. ...prev,
  722. [lotKey]: 0
  723. }));
  724. return;
  725. }
  726. setPickQtyData(prev => ({
  727. ...prev,
  728. [lotKey]: numericValue
  729. }));
  730. }, []);
  731. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  732. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  733. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  734. const checkAndAutoAssignNext = useCallback(async () => {
  735. if (!currentUserId) return;
  736. try {
  737. const completionResponse = await checkPickOrderCompletion(currentUserId);
  738. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  739. console.log("Found completed pick orders, auto-assigning next...");
  740. // ✅ 移除前端的自动分配逻辑,因为后端已经处理了
  741. // await handleAutoAssignAndRelease(); // 删除这个函数
  742. }
  743. } catch (error) {
  744. console.error("Error checking pick order completion:", error);
  745. }
  746. }, [currentUserId]);
  747. // ✅ Handle submit pick quantity
  748. const handleSubmitPickQty = useCallback(async (lot: any) => {
  749. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  750. const newQty = pickQtyData[lotKey] || 0;
  751. if (!lot.stockOutLineId) {
  752. console.error("No stock out line found for this lot");
  753. return;
  754. }
  755. try {
  756. const currentActualPickQty = lot.actualPickQty || 0;
  757. const cumulativeQty = currentActualPickQty + newQty;
  758. let newStatus = 'partially_completed';
  759. if (cumulativeQty >= lot.requiredQty) {
  760. newStatus = 'completed';
  761. }
  762. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  763. console.log(`Lot: ${lot.lotNo}`);
  764. console.log(`Required Qty: ${lot.requiredQty}`);
  765. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  766. console.log(`New Submitted Qty: ${newQty}`);
  767. console.log(`Cumulative Qty: ${cumulativeQty}`);
  768. console.log(`New Status: ${newStatus}`);
  769. console.log(`=====================================`);
  770. await updateStockOutLineStatus({
  771. id: lot.stockOutLineId,
  772. status: newStatus,
  773. qty: cumulativeQty
  774. });
  775. if (newQty > 0) {
  776. await updateInventoryLotLineQuantities({
  777. inventoryLotLineId: lot.lotId,
  778. qty: newQty,
  779. status: 'available',
  780. operation: 'pick'
  781. });
  782. }
  783. // ✅ FIXED: Use the proper API function instead of direct fetch
  784. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  785. console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  786. try {
  787. // ✅ Use the imported API function instead of direct fetch
  788. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  789. console.log(`✅ Pick order completion check result:`, completionResponse);
  790. if (completionResponse.code === "SUCCESS") {
  791. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  792. } else if (completionResponse.message === "not completed") {
  793. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  794. } else {
  795. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  796. }
  797. } catch (error) {
  798. console.error("Error checking pick order completion:", error);
  799. }
  800. }
  801. await fetchJobOrderData();
  802. console.log("Pick quantity submitted successfully!");
  803. setTimeout(() => {
  804. checkAndAutoAssignNext();
  805. }, 1000);
  806. } catch (error) {
  807. console.error("Error submitting pick quantity:", error);
  808. }
  809. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  810. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  811. if (!lot.stockOutLineId) {
  812. console.error("No stock out line found for this lot");
  813. return;
  814. }
  815. try {
  816. // ✅ FIXED: Calculate cumulative quantity correctly
  817. const currentActualPickQty = lot.actualPickQty || 0;
  818. const cumulativeQty = currentActualPickQty + submitQty;
  819. // ✅ FIXED: Determine status based on cumulative quantity vs required quantity
  820. let newStatus = 'partially_completed';
  821. if (cumulativeQty >= lot.requiredQty) {
  822. newStatus = 'completed';
  823. } else if (cumulativeQty > 0) {
  824. newStatus = 'partially_completed';
  825. } else {
  826. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  827. }
  828. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  829. console.log(`Lot: ${lot.lotNo}`);
  830. console.log(`Required Qty: ${lot.requiredQty}`);
  831. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  832. console.log(`New Submitted Qty: ${submitQty}`);
  833. console.log(`Cumulative Qty: ${cumulativeQty}`);
  834. console.log(`New Status: ${newStatus}`);
  835. console.log(`=====================================`);
  836. await updateStockOutLineStatus({
  837. id: lot.stockOutLineId,
  838. status: newStatus,
  839. qty: cumulativeQty // ✅ Use cumulative quantity
  840. });
  841. if (submitQty > 0) {
  842. await updateInventoryLotLineQuantities({
  843. inventoryLotLineId: lot.lotId,
  844. qty: submitQty,
  845. status: 'available',
  846. operation: 'pick'
  847. });
  848. }
  849. // ✅ Check if pick order is completed when lot status becomes 'completed'
  850. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  851. console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  852. try {
  853. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  854. console.log(`✅ Pick order completion check result:`, completionResponse);
  855. if (completionResponse.code === "SUCCESS") {
  856. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  857. } else if (completionResponse.message === "not completed") {
  858. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  859. } else {
  860. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  861. }
  862. } catch (error) {
  863. console.error("Error checking pick order completion:", error);
  864. }
  865. }
  866. await fetchJobOrderData();
  867. console.log("Pick quantity submitted successfully!");
  868. setTimeout(() => {
  869. checkAndAutoAssignNext();
  870. }, 1000);
  871. } catch (error) {
  872. console.error("Error submitting pick quantity:", error);
  873. }
  874. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  875. // ✅ Handle reject lot
  876. const handleRejectLot = useCallback(async (lot: any) => {
  877. if (!lot.stockOutLineId) {
  878. console.error("No stock out line found for this lot");
  879. return;
  880. }
  881. try {
  882. await updateStockOutLineStatus({
  883. id: lot.stockOutLineId,
  884. status: 'rejected',
  885. qty: 0
  886. });
  887. await fetchJobOrderData();
  888. console.log("Lot rejected successfully!");
  889. setTimeout(() => {
  890. checkAndAutoAssignNext();
  891. }, 1000);
  892. } catch (error) {
  893. console.error("Error rejecting lot:", error);
  894. }
  895. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  896. // ✅ Handle pick execution form
  897. const handlePickExecutionForm = useCallback((lot: any) => {
  898. console.log("=== Pick Execution Form ===");
  899. console.log("Lot data:", lot);
  900. if (!lot) {
  901. console.warn("No lot data provided for pick execution form");
  902. return;
  903. }
  904. console.log("Opening pick execution form for lot:", lot.lotNo);
  905. setSelectedLotForExecutionForm(lot);
  906. setPickExecutionFormOpen(true);
  907. console.log("Pick execution form opened for lot ID:", lot.lotId);
  908. }, []);
  909. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  910. try {
  911. console.log("Pick execution form submitted:", data);
  912. const result = await recordPickExecutionIssue(data);
  913. console.log("Pick execution issue recorded:", result);
  914. if (result && result.code === "SUCCESS") {
  915. console.log("✅ Pick execution issue recorded successfully");
  916. } else {
  917. console.error("❌ Failed to record pick execution issue:", result);
  918. }
  919. setPickExecutionFormOpen(false);
  920. setSelectedLotForExecutionForm(null);
  921. await fetchJobOrderData();
  922. } catch (error) {
  923. console.error("Error submitting pick execution form:", error);
  924. }
  925. }, [fetchJobOrderData]);
  926. // ✅ Calculate remaining required quantity
  927. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  928. const requiredQty = lot.requiredQty || 0;
  929. const stockOutLineQty = lot.stockOutLineQty || 0;
  930. return Math.max(0, requiredQty - stockOutLineQty);
  931. }, []);
  932. // Search criteria
  933. const searchCriteria: Criterion<any>[] = [
  934. {
  935. label: t("Pick Order Code"),
  936. paramName: "pickOrderCode",
  937. type: "text",
  938. },
  939. {
  940. label: t("Item Code"),
  941. paramName: "itemCode",
  942. type: "text",
  943. },
  944. {
  945. label: t("Item Name"),
  946. paramName: "itemName",
  947. type: "text",
  948. },
  949. {
  950. label: t("Lot No"),
  951. paramName: "lotNo",
  952. type: "text",
  953. },
  954. ];
  955. const handleSearch = useCallback((query: Record<string, any>) => {
  956. setSearchQuery({ ...query });
  957. console.log("Search query:", query);
  958. if (!originalCombinedData) return;
  959. const filtered = originalCombinedData.filter((lot: any) => {
  960. const pickOrderCodeMatch = !query.pickOrderCode ||
  961. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  962. const itemCodeMatch = !query.itemCode ||
  963. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  964. const itemNameMatch = !query.itemName ||
  965. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  966. const lotNoMatch = !query.lotNo ||
  967. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  968. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  969. });
  970. setCombinedLotData(filtered);
  971. console.log("Filtered lots count:", filtered.length);
  972. }, [originalCombinedData]);
  973. const handleReset = useCallback(() => {
  974. setSearchQuery({});
  975. if (originalCombinedData) {
  976. setCombinedLotData(originalCombinedData);
  977. }
  978. }, [originalCombinedData]);
  979. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  980. setPaginationController(prev => ({
  981. ...prev,
  982. pageNum: newPage,
  983. }));
  984. }, []);
  985. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  986. const newPageSize = parseInt(event.target.value, 10);
  987. setPaginationController({
  988. pageNum: 0,
  989. pageSize: newPageSize,
  990. });
  991. }, []);
  992. // Pagination data with sorting by routerIndex
  993. const paginatedData = useMemo(() => {
  994. // ✅ Sort by routerIndex first, then by other criteria
  995. const sortedData = [...combinedLotData].sort((a, b) => {
  996. const aIndex = a.routerIndex || 0;
  997. const bIndex = b.routerIndex || 0;
  998. // Primary sort: by routerIndex
  999. if (aIndex !== bIndex) {
  1000. return aIndex - bIndex;
  1001. }
  1002. // Secondary sort: by pickOrderCode if routerIndex is the same
  1003. if (a.pickOrderCode !== b.pickOrderCode) {
  1004. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  1005. }
  1006. // Tertiary sort: by lotNo if everything else is the same
  1007. return (a.lotNo || '').localeCompare(b.lotNo || '');
  1008. });
  1009. const startIndex = paginationController.pageNum * paginationController.pageSize;
  1010. const endIndex = startIndex + paginationController.pageSize;
  1011. return sortedData.slice(startIndex, endIndex);
  1012. }, [combinedLotData, paginationController]);
  1013. // ✅ Add these functions for manual scanning
  1014. const handleStartScan = useCallback(() => {
  1015. console.log(" Starting manual QR scan...");
  1016. setIsManualScanning(true);
  1017. setProcessedQrCodes(new Set());
  1018. setLastProcessedQr('');
  1019. setQrScanError(false);
  1020. setQrScanSuccess(false);
  1021. startScan();
  1022. }, [startScan]);
  1023. const handleStopScan = useCallback(() => {
  1024. console.log("⏹️ Stopping manual QR scan...");
  1025. setIsManualScanning(false);
  1026. setQrScanError(false);
  1027. setQrScanSuccess(false);
  1028. stopScan();
  1029. resetScan();
  1030. }, [stopScan, resetScan]);
  1031. const getStatusMessage = useCallback((lot: any) => {
  1032. switch (lot.stockOutLineStatus?.toLowerCase()) {
  1033. case 'pending':
  1034. return t("Please finish QR code scan and pick order.");
  1035. case 'checked':
  1036. return t("Please submit the pick order.");
  1037. case 'partially_completed':
  1038. return t("Partial quantity submitted. Please submit more or complete the order.");
  1039. case 'completed':
  1040. return t("Pick order completed successfully!");
  1041. case 'rejected':
  1042. return t("Lot has been rejected and marked as unavailable.");
  1043. case 'unavailable':
  1044. return t("This order is insufficient, please pick another lot.");
  1045. default:
  1046. return t("Please finish QR code scan and pick order.");
  1047. }
  1048. }, [t]);
  1049. return (
  1050. <FormProvider {...formProps}>
  1051. <Stack spacing={2}>
  1052. {/* Job Order Header */}
  1053. {jobOrderData && (
  1054. <Paper sx={{ p: 2 }}>
  1055. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  1056. <Typography variant="subtitle1">
  1057. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.name || '-'}
  1058. </Typography>
  1059. <Typography variant="subtitle1">
  1060. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  1061. </Typography>
  1062. <Typography variant="subtitle1">
  1063. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  1064. </Typography>
  1065. <Typography variant="subtitle1">
  1066. <strong>{t("Status")}:</strong> {jobOrderData.pickOrder?.status || '-'}
  1067. </Typography>
  1068. </Stack>
  1069. </Paper>
  1070. )}
  1071. {/* Combined Lot Table */}
  1072. <Box>
  1073. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1074. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1075. {!isManualScanning ? (
  1076. <Button
  1077. variant="contained"
  1078. startIcon={<QrCodeIcon />}
  1079. onClick={handleStartScan}
  1080. color="primary"
  1081. sx={{ minWidth: '120px' }}
  1082. >
  1083. {t("Start QR Scan")}
  1084. </Button>
  1085. ) : (
  1086. <Button
  1087. variant="outlined"
  1088. startIcon={<QrCodeIcon />}
  1089. onClick={handleStopScan}
  1090. color="secondary"
  1091. sx={{ minWidth: '120px' }}
  1092. >
  1093. {t("Stop QR Scan")}
  1094. </Button>
  1095. )}
  1096. {isManualScanning && (
  1097. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  1098. <CircularProgress size={16} />
  1099. <Typography variant="caption" color="primary">
  1100. {t("Scanning...")}
  1101. </Typography>
  1102. </Box>
  1103. )}
  1104. </Box>
  1105. </Box>
  1106. {qrScanError && !qrScanSuccess && (
  1107. <Alert severity="error" sx={{ mb: 2 }}>
  1108. {t("QR code does not match any item in current orders.")}
  1109. </Alert>
  1110. )}
  1111. {qrScanSuccess && (
  1112. <Alert severity="success" sx={{ mb: 2 }}>
  1113. {t("QR code verified.")}
  1114. </Alert>
  1115. )}
  1116. <TableContainer component={Paper}>
  1117. <Table>
  1118. <TableHead>
  1119. <TableRow>
  1120. <TableCell>{t("Index")}</TableCell>
  1121. <TableCell>{t("Route")}</TableCell>
  1122. <TableCell>{t("Item Code")}</TableCell>
  1123. <TableCell>{t("Item Name")}</TableCell>
  1124. <TableCell>{t("Lot No")}</TableCell>
  1125. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  1126. <TableCell align="center">{t("Scan Result")}</TableCell>
  1127. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  1128. </TableRow>
  1129. </TableHead>
  1130. <TableBody>
  1131. {paginatedData.length === 0 ? (
  1132. <TableRow>
  1133. <TableCell colSpan={8} align="center">
  1134. <Typography variant="body2" color="text.secondary">
  1135. {t("No data available")}
  1136. </Typography>
  1137. </TableCell>
  1138. </TableRow>
  1139. ) : (
  1140. paginatedData.map((lot, index) => (
  1141. <TableRow
  1142. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  1143. sx={{
  1144. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  1145. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  1146. '& .MuiTableCell-root': {
  1147. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  1148. }
  1149. }}
  1150. >
  1151. <TableCell>
  1152. <Typography variant="body2" fontWeight="bold">
  1153. {index + 1}
  1154. </Typography>
  1155. </TableCell>
  1156. <TableCell>
  1157. <Typography variant="body2">
  1158. {lot.routerRoute || '-'}
  1159. </Typography>
  1160. </TableCell>
  1161. <TableCell>{lot.itemCode}</TableCell>
  1162. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  1163. <TableCell>
  1164. <Box>
  1165. <Typography
  1166. sx={{
  1167. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  1168. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  1169. }}
  1170. >
  1171. {lot.lotNo}
  1172. </Typography>
  1173. </Box>
  1174. </TableCell>
  1175. <TableCell align="right">
  1176. {(() => {
  1177. const requiredQty = lot.requiredQty || 0;
  1178. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  1179. })()}
  1180. </TableCell>
  1181. <TableCell align="center">
  1182. {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
  1183. <Box sx={{
  1184. display: 'flex',
  1185. justifyContent: 'center',
  1186. alignItems: 'center',
  1187. width: '100%',
  1188. height: '100%'
  1189. }}>
  1190. <Checkbox
  1191. checked={lot.stockOutLineStatus?.toLowerCase() !== 'pending'}
  1192. disabled={true}
  1193. readOnly={true}
  1194. size="large"
  1195. sx={{
  1196. color: lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400',
  1197. '&.Mui-checked': {
  1198. color: 'success.main',
  1199. },
  1200. transform: 'scale(1.3)',
  1201. '& .MuiSvgIcon-root': {
  1202. fontSize: '1.5rem',
  1203. }
  1204. }}
  1205. />
  1206. </Box>
  1207. ) : null}
  1208. </TableCell>
  1209. <TableCell align="center">
  1210. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  1211. <Stack direction="row" spacing={1} alignItems="center">
  1212. <Button
  1213. variant="contained"
  1214. onClick={() => {
  1215. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1216. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1217. // Submit with default lot required pick qty
  1218. handlePickQtyChange(lotKey, submitQty);
  1219. handleSubmitPickQtyWithQty(lot, submitQty);
  1220. }}
  1221. disabled={
  1222. (lot.lotAvailability === 'expired' ||
  1223. lot.lotAvailability === 'status_unavailable' ||
  1224. lot.lotAvailability === 'rejected') ||
  1225. lot.stockOutLineStatus === 'completed' ||
  1226. lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed
  1227. }
  1228. sx={{
  1229. fontSize: '0.75rem',
  1230. py: 0.5,
  1231. minHeight: '28px',
  1232. minWidth: '70px'
  1233. }}
  1234. >
  1235. {t("Submit")}
  1236. </Button>
  1237. <Button
  1238. variant="outlined"
  1239. size="small"
  1240. onClick={() => handlePickExecutionForm(lot)}
  1241. disabled={
  1242. (lot.lotAvailability === 'expired' ||
  1243. lot.lotAvailability === 'status_unavailable' ||
  1244. lot.lotAvailability === 'rejected') ||
  1245. lot.stockOutLineStatus === 'completed' || // ✅ Disable when finished
  1246. lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed
  1247. }
  1248. sx={{
  1249. fontSize: '0.7rem',
  1250. py: 0.5,
  1251. minHeight: '28px',
  1252. minWidth: '60px',
  1253. borderColor: 'warning.main',
  1254. color: 'warning.main'
  1255. }}
  1256. title="Report missing or bad items"
  1257. >
  1258. {t("Issue")}
  1259. </Button>
  1260. </Stack>
  1261. </Box>
  1262. </TableCell>
  1263. </TableRow>
  1264. ))
  1265. )}
  1266. </TableBody>
  1267. </Table>
  1268. </TableContainer>
  1269. <TablePagination
  1270. component="div"
  1271. count={combinedLotData.length}
  1272. page={paginationController.pageNum}
  1273. rowsPerPage={paginationController.pageSize}
  1274. onPageChange={handlePageChange}
  1275. onRowsPerPageChange={handlePageSizeChange}
  1276. rowsPerPageOptions={[10, 25, 50]}
  1277. labelRowsPerPage={t("Rows per page")}
  1278. labelDisplayedRows={({ from, to, count }) =>
  1279. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1280. }
  1281. />
  1282. </Box>
  1283. </Stack>
  1284. {/* ✅ QR Code Modal */}
  1285. <QrCodeModal
  1286. open={qrModalOpen}
  1287. onClose={() => {
  1288. setQrModalOpen(false);
  1289. setSelectedLotForQr(null);
  1290. stopScan();
  1291. resetScan();
  1292. }}
  1293. lot={selectedLotForQr}
  1294. combinedLotData={combinedLotData}
  1295. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  1296. />
  1297. {/* ✅ Pick Execution Form Modal */}
  1298. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  1299. <GoodPickExecutionForm
  1300. open={pickExecutionFormOpen}
  1301. onClose={() => {
  1302. setPickExecutionFormOpen(false);
  1303. setSelectedLotForExecutionForm(null);
  1304. }}
  1305. onSubmit={handlePickExecutionFormSubmit}
  1306. selectedLot={selectedLotForExecutionForm}
  1307. selectedPickOrderLine={{
  1308. id: selectedLotForExecutionForm.pickOrderLineId,
  1309. itemId: selectedLotForExecutionForm.itemId,
  1310. itemCode: selectedLotForExecutionForm.itemCode,
  1311. itemName: selectedLotForExecutionForm.itemName,
  1312. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  1313. // ✅ Add missing required properties from GetPickOrderLineInfo interface
  1314. availableQty: selectedLotForExecutionForm.availableQty || 0,
  1315. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  1316. uomCode: selectedLotForExecutionForm.uomCode || '',
  1317. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  1318. pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // ✅ Use pickedQty instead of actualPickQty
  1319. suggestedList: [] // ✅ Add required suggestedList property
  1320. }}
  1321. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  1322. pickOrderCreateDate={new Date()}
  1323. />
  1324. )}
  1325. </FormProvider>
  1326. );
  1327. };
  1328. export default JobPickExecution