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

JobPickExecutionsecondscan.tsx 49 KiB

6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
3ヶ月前
1ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
3ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
1ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
1ヶ月前
3ヶ月前
6ヶ月前
1ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
5ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
5ヶ月前
6ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395
  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 TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  22. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import { useRouter } from "next/navigation";
  25. import ArrowBackIcon from '@mui/icons-material/ArrowBack';
  26. // 修改:使用 Job Order API
  27. import {
  28. fetchCompletedJobOrderPickOrders,
  29. updateSecondQrScanStatus,
  30. submitSecondScanQuantity,
  31. recordSecondScanIssue,
  32. unAssignJobOrderPickOrder,
  33. fetchJobOrderLotsHierarchicalByPickOrderId
  34. } from "@/app/api/jo/actions";
  35. import { fetchNameList, NameList } from "@/app/api/user/actions";
  36. import {
  37. FormProvider,
  38. useForm,
  39. } from "react-hook-form";
  40. import SearchBox, { Criterion } from "../SearchBox";
  41. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  42. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  43. import QrCodeIcon from '@mui/icons-material/QrCode';
  44. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  45. import { useSession } from "next-auth/react";
  46. import { SessionWithTokens } from "@/config/authConfig";
  47. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  48. import GoodPickExecutionForm from "./JobmatchForm";
  49. import { BASE_API_URL } from "@/config/api";
  50. interface Props {
  51. filterArgs: Record<string, any>;
  52. onBack?: () => void; // 添加返回回调
  53. }
  54. // QR Code Modal Component (from GoodPickExecution)
  55. const QrCodeModal: React.FC<{
  56. open: boolean;
  57. onClose: () => void;
  58. lot: any | null;
  59. onQrCodeSubmit: (lotNo: string) => void;
  60. combinedLotData: any[];
  61. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  62. const { t } = useTranslation("jo");
  63. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  64. const [manualInput, setManualInput] = useState<string>('');
  65. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  66. const [manualInputError, setManualInputError] = useState<boolean>(false);
  67. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  68. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  69. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  70. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  71. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  72. const { data: session } = useSession() as { data: SessionWithTokens | null };
  73. // Process scanned QR codes
  74. useEffect(() => {
  75. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  76. const latestQr = qrValues[qrValues.length - 1];
  77. if (processedQrCodes.has(latestQr)) {
  78. console.log("QR code already processed, skipping...");
  79. return;
  80. }
  81. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  82. try {
  83. const qrData = JSON.parse(latestQr);
  84. if (qrData.stockInLineId && qrData.itemId) {
  85. setIsProcessingQr(true);
  86. setQrScanFailed(false);
  87. fetchStockInLineInfo(qrData.stockInLineId)
  88. .then((stockInLineInfo) => {
  89. console.log("Stock in line info:", stockInLineInfo);
  90. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  91. if (stockInLineInfo.lotNo === lot.lotNo) {
  92. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  93. setQrScanSuccess(true);
  94. onQrCodeSubmit(lot.lotNo);
  95. onClose();
  96. resetScan();
  97. } else {
  98. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  99. setQrScanFailed(true);
  100. setManualInputError(true);
  101. setManualInputSubmitted(true);
  102. }
  103. })
  104. .catch((error) => {
  105. console.error("Error fetching stock in line info:", error);
  106. setScannedQrResult('Error fetching data');
  107. setQrScanFailed(true);
  108. setManualInputError(true);
  109. setManualInputSubmitted(true);
  110. })
  111. .finally(() => {
  112. setIsProcessingQr(false);
  113. });
  114. } else {
  115. const qrContent = latestQr.replace(/[{}]/g, '');
  116. setScannedQrResult(qrContent);
  117. if (qrContent === lot.lotNo) {
  118. setQrScanSuccess(true);
  119. onQrCodeSubmit(lot.lotNo);
  120. onClose();
  121. resetScan();
  122. } else {
  123. setQrScanFailed(true);
  124. setManualInputError(true);
  125. setManualInputSubmitted(true);
  126. }
  127. }
  128. } catch (error) {
  129. console.log("QR code is not JSON format, trying direct comparison");
  130. const qrContent = latestQr.replace(/[{}]/g, '');
  131. setScannedQrResult(qrContent);
  132. if (qrContent === lot.lotNo) {
  133. setQrScanSuccess(true);
  134. onQrCodeSubmit(lot.lotNo);
  135. onClose();
  136. resetScan();
  137. } else {
  138. setQrScanFailed(true);
  139. setManualInputError(true);
  140. setManualInputSubmitted(true);
  141. }
  142. }
  143. }
  144. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  145. // Clear states when modal opens
  146. useEffect(() => {
  147. if (open) {
  148. setManualInput('');
  149. setManualInputSubmitted(false);
  150. setManualInputError(false);
  151. setIsProcessingQr(false);
  152. setQrScanFailed(false);
  153. setQrScanSuccess(false);
  154. setScannedQrResult('');
  155. setProcessedQrCodes(new Set());
  156. }
  157. }, [open]);
  158. useEffect(() => {
  159. if (lot) {
  160. setManualInput('');
  161. setManualInputSubmitted(false);
  162. setManualInputError(false);
  163. setIsProcessingQr(false);
  164. setQrScanFailed(false);
  165. setQrScanSuccess(false);
  166. setScannedQrResult('');
  167. setProcessedQrCodes(new Set());
  168. }
  169. }, [lot]);
  170. // Auto-submit manual input when it matches
  171. useEffect(() => {
  172. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  173. console.log(' Auto-submitting manual input:', manualInput.trim());
  174. const timer = setTimeout(() => {
  175. setQrScanSuccess(true);
  176. onQrCodeSubmit(lot.lotNo);
  177. onClose();
  178. setManualInput('');
  179. setManualInputError(false);
  180. setManualInputSubmitted(false);
  181. }, 200);
  182. return () => clearTimeout(timer);
  183. }
  184. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  185. const handleManualSubmit = () => {
  186. if (manualInput.trim() === lot?.lotNo) {
  187. setQrScanSuccess(true);
  188. onQrCodeSubmit(lot.lotNo);
  189. onClose();
  190. setManualInput('');
  191. } else {
  192. setQrScanFailed(true);
  193. setManualInputError(true);
  194. setManualInputSubmitted(true);
  195. }
  196. };
  197. useEffect(() => {
  198. if (open) {
  199. startScan();
  200. }
  201. }, [open, startScan]);
  202. return (
  203. <Modal open={open} onClose={onClose}>
  204. <Box sx={{
  205. position: 'absolute',
  206. top: '50%',
  207. left: '50%',
  208. transform: 'translate(-50%, -50%)',
  209. bgcolor: 'background.paper',
  210. p: 3,
  211. borderRadius: 2,
  212. minWidth: 400,
  213. }}>
  214. <Typography variant="h6" gutterBottom>
  215. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  216. </Typography>
  217. {isProcessingQr && (
  218. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  219. <Typography variant="body2" color="primary">
  220. {t("Processing QR code...")}
  221. </Typography>
  222. </Box>
  223. )}
  224. <Box sx={{ mb: 2 }}>
  225. <Typography variant="body2" gutterBottom>
  226. <strong>{t("Manual Input")}:</strong>
  227. </Typography>
  228. <TextField
  229. fullWidth
  230. size="small"
  231. value={manualInput}
  232. onChange={(e) => {
  233. setManualInput(e.target.value);
  234. if (qrScanFailed || manualInputError) {
  235. setQrScanFailed(false);
  236. setManualInputError(false);
  237. setManualInputSubmitted(false);
  238. }
  239. }}
  240. sx={{ mb: 1 }}
  241. error={manualInputSubmitted && manualInputError}
  242. helperText={
  243. manualInputSubmitted && manualInputError
  244. ? `${t("The input is not the same as the expected lot number.")}`
  245. : ''
  246. }
  247. />
  248. <Button
  249. variant="contained"
  250. onClick={handleManualSubmit}
  251. disabled={!manualInput.trim()}
  252. size="small"
  253. color="primary"
  254. >
  255. {t("Submit")}
  256. </Button>
  257. </Box>
  258. {qrValues.length > 0 && (
  259. <Box sx={{
  260. mb: 2,
  261. p: 2,
  262. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  263. borderRadius: 1
  264. }}>
  265. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  266. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  267. </Typography>
  268. {qrScanSuccess && (
  269. <Typography variant="caption" color="success" display="block">
  270. {t("Verified successfully!")}
  271. </Typography>
  272. )}
  273. </Box>
  274. )}
  275. <Box sx={{ mt: 2, textAlign: 'right' }}>
  276. <Button onClick={onClose} variant="outlined">
  277. {t("Cancel")}
  278. </Button>
  279. </Box>
  280. </Box>
  281. </Modal>
  282. );
  283. };
  284. const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
  285. const { t } = useTranslation("jo");
  286. const router = useRouter();
  287. const { data: session } = useSession() as { data: SessionWithTokens | null };
  288. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  289. const authTokenRef = useRef<string | null>(null);
  290. // 修改:使用 Job Order 数据结构
  291. const [jobOrderData, setJobOrderData] = useState<any>(null);
  292. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  293. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  294. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  295. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  296. const [qrScanInput, setQrScanInput] = useState<string>('');
  297. const [qrScanError, setQrScanError] = useState<boolean>(false);
  298. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  299. const unassignExecutedRef = useRef(false);
  300. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  301. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  302. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  303. const [paginationController, setPaginationController] = useState({
  304. pageNum: 0,
  305. pageSize: 10,
  306. });
  307. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  308. const initializationRef = useRef(false);
  309. const formProps = useForm();
  310. const errors = formProps.formState.errors;
  311. // Add QR modal states
  312. const [qrModalOpen, setQrModalOpen] = useState(false);
  313. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  314. // Add GoodPickExecutionForm states
  315. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  316. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  317. // Add these missing state variables
  318. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  319. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  320. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  321. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  322. const [currentPickOrderId, setCurrentPickOrderId] = useState<number | null>(null);
  323. // 添加:unassign 函数
  324. const handleUnassign = useCallback(async (pickOrderId: number | null) => {
  325. if (!pickOrderId || !currentUserId) {
  326. console.log("No pickOrderId or userId to unassign");
  327. return;
  328. }
  329. try {
  330. console.log(`🔄 Unassigning pick order: ${pickOrderId} for user: ${currentUserId}`);
  331. await unAssignJobOrderPickOrder(pickOrderId);
  332. console.log(`✅ Successfully unassigned pick order: ${pickOrderId}`);
  333. } catch (error) {
  334. console.error(`❌ Error unassigning pick order ${pickOrderId}:`, error);
  335. // 即使 unassign 失败,也继续执行,不阻塞用户操作
  336. }
  337. }, [currentUserId]);
  338. useEffect(() => {
  339. if (session && (session as any).accessToken) {
  340. authTokenRef.current = (session as any).accessToken;
  341. }
  342. }, [session]);
  343. const handleUnassignSync = useCallback((pickOrderId: number | null) => {
  344. if (!pickOrderId || !currentUserId) {
  345. return;
  346. }
  347. const url = `${BASE_API_URL}/jo/unassign-job-order-pick-order/${pickOrderId}`;
  348. const headers: HeadersInit = {
  349. 'Content-Type': 'application/json',
  350. };
  351. if (authTokenRef.current) {
  352. headers['Authorization'] = `Bearer ${authTokenRef.current}`;
  353. }
  354. fetch(url, {
  355. method: 'POST',
  356. headers: headers,
  357. keepalive: true,
  358. body: JSON.stringify({})
  359. }).catch((error) => {
  360. console.error(`❌ Error in sync unassign:`, error);
  361. // 备用方案:sendBeacon(但无法发送自定义 headers)
  362. if (navigator.sendBeacon) {
  363. const blob = new Blob([JSON.stringify({})], { type: 'application/json' });
  364. navigator.sendBeacon(url, blob);
  365. }
  366. });
  367. }, [currentUserId]);
  368. // 修改:使用 Job Order API 获取数据
  369. const fetchJobOrderData = useCallback(async (userId?: number) => {
  370. setCombinedDataLoading(true);
  371. try {
  372. const userIdToUse = userId || currentUserId;
  373. console.log(" fetchJobOrderData called with userId:", userIdToUse);
  374. if (!userIdToUse) {
  375. console.warn("No userId available, skipping API call");
  376. setJobOrderData(null);
  377. setCombinedLotData([]);
  378. setOriginalCombinedData([]);
  379. setCurrentPickOrderId(null);
  380. return;
  381. }
  382. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  383. detail: {
  384. hasData: false,
  385. tabIndex: 1
  386. }
  387. }));
  388. // 使用 Job Order API
  389. const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
  390. console.log(" Job Order data:", jobOrderData);
  391. console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code);
  392. setJobOrderData(jobOrderData);
  393. // 保存 pickOrderId
  394. if (jobOrderData?.pickOrder?.id) {
  395. setCurrentPickOrderId(jobOrderData.pickOrder.id);
  396. }
  397. // Transform hierarchical data to flat structure for the table
  398. const flatLotData: any[] = [];
  399. if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
  400. jobOrderData.pickOrderLines.forEach((line: any) => {
  401. if (line.lots && line.lots.length > 0) {
  402. line.lots.forEach((lot: any) => {
  403. flatLotData.push({
  404. // Pick order info
  405. pickOrderId: jobOrderData.pickOrder.id,
  406. pickOrderCode: jobOrderData.pickOrder.code,
  407. pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
  408. pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
  409. pickOrderType: jobOrderData.pickOrder.type,
  410. pickOrderStatus: jobOrderData.pickOrder.status,
  411. pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
  412. // Pick order line info
  413. pickOrderLineId: line.id,
  414. pickOrderLineRequiredQty: line.requiredQty,
  415. pickOrderLineStatus: line.status,
  416. // Item info
  417. itemId: line.itemId,
  418. itemCode: line.itemCode,
  419. itemName: line.itemName,
  420. uomCode: line.uomCode,
  421. uomDesc: line.uomDesc,
  422. // Lot info
  423. lotId: lot.lotId,
  424. lotNo: lot.lotNo,
  425. expiryDate: lot.expiryDate,
  426. location: lot.location,
  427. availableQty: lot.availableQty,
  428. requiredQty: lot.requiredQty,
  429. actualPickQty: lot.actualPickQty,
  430. lotStatus: lot.lotStatus,
  431. lotAvailability: lot.lotAvailability,
  432. processingStatus: lot.processingStatus,
  433. stockOutLineId: lot.stockOutLineId,
  434. stockOutLineStatus: lot.stockOutLineStatus,
  435. stockOutLineQty: lot.stockOutLineQty,
  436. // Router info
  437. routerIndex: lot.routerIndex,
  438. matchStatus: lot.matchStatus,
  439. routerArea: lot.routerArea,
  440. routerRoute: lot.routerRoute,
  441. uomShortDesc: lot.uomShortDesc,
  442. handler: lot.handler,
  443. });
  444. });
  445. }
  446. });
  447. }
  448. console.log(" Transformed flat lot data:", flatLotData);
  449. setCombinedLotData(flatLotData);
  450. setOriginalCombinedData(flatLotData);
  451. const hasData = flatLotData.length > 0;
  452. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  453. detail: {
  454. hasData: hasData,
  455. tabIndex: 1
  456. }
  457. }));
  458. // 计算完成状态并发送事件
  459. const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
  460. lot.processingStatus === 'completed'
  461. );
  462. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  463. detail: {
  464. allLotsCompleted: allCompleted,
  465. tabIndex: 0
  466. }
  467. }));
  468. } catch (error: any) {
  469. console.error("❌ Error fetching job order data:", error);
  470. // 检查是否是 token 错误或认证错误
  471. const isAuthError = error?.status === 401 ||
  472. error?.status === 403 ||
  473. error?.message?.toLowerCase().includes('unauthorized') ||
  474. error?.message?.toLowerCase().includes('token');
  475. if (isAuthError) {
  476. console.log("🔒 Authentication error detected, unassigning pick order");
  477. // Token 错误时也要 unassign
  478. await handleUnassign(currentPickOrderId);
  479. }
  480. setJobOrderData(null);
  481. setCombinedLotData([]);
  482. setOriginalCombinedData([]);
  483. setCurrentPickOrderId(null);
  484. // 如果加载失败,禁用打印按钮
  485. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  486. detail: {
  487. allLotsCompleted: false,
  488. tabIndex: 0
  489. }
  490. }));
  491. } finally {
  492. setCombinedDataLoading(false);
  493. }
  494. }, [currentUserId, currentPickOrderId, handleUnassign]);
  495. useEffect(() => {
  496. return () => {
  497. // Cleanup QR scanner
  498. if (isManualScanning) {
  499. console.log("🧹 Second scan component unmounting, stopping QR scanner...");
  500. stopScan();
  501. resetScan();
  502. }
  503. };
  504. }, [isManualScanning, stopScan, resetScan]);
  505. // 添加:处理返回按钮
  506. const handleBack = useCallback(async () => {
  507. if (currentPickOrderId && currentUserId) {
  508. console.log("🔄 Back button clicked, unassigning pick order:", currentPickOrderId);
  509. await handleUnassign(currentPickOrderId);
  510. }
  511. if (onBack) {
  512. onBack();
  513. }
  514. }, [currentPickOrderId, currentUserId, handleUnassign, onBack]);
  515. const handleSubmitAllScanned = useCallback(async () => {
  516. const scannedLots = combinedLotData.filter(lot =>
  517. lot.matchStatus === 'scanned'||
  518. lot.stockOutLineStatus === 'completed'
  519. );
  520. if (scannedLots.length === 0) {
  521. console.log("No scanned items to submit");
  522. return;
  523. }
  524. setIsSubmittingAll(true);
  525. console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
  526. try {
  527. const submitPromises = scannedLots.map(async (lot) => {
  528. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  529. console.log(`Submitting item ${lot.itemCode}: qty=${submitQty}`);
  530. const result = await submitSecondScanQuantity(
  531. lot.pickOrderId,
  532. lot.itemId,
  533. {
  534. qty: submitQty,
  535. userId: currentUserId !!,
  536. isMissing: false,
  537. isBad: false,
  538. reason: undefined
  539. }
  540. );
  541. return { success: result.code === "SUCCESS", itemCode: lot.itemCode };
  542. });
  543. const results = await Promise.all(submitPromises);
  544. const successCount = results.filter(r => r.success).length;
  545. console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
  546. await fetchJobOrderData();
  547. if (successCount > 0) {
  548. setQrScanSuccess(true);
  549. setTimeout(() => {
  550. setQrScanSuccess(false);
  551. // 添加:提交成功后返回到列表
  552. if (onBack) {
  553. onBack();
  554. }
  555. }, 2000);
  556. }
  557. } catch (error: any) {
  558. console.error("Error submitting all scanned items:", error);
  559. const isAuthError = error?.status === 401 ||
  560. error?.status === 403 ||
  561. error?.message?.toLowerCase().includes('unauthorized') ||
  562. error?.message?.toLowerCase().includes('token');
  563. if (isAuthError) {
  564. console.log("🔒 Authentication error in submit, unassigning pick order");
  565. await handleUnassign(currentPickOrderId);
  566. }
  567. setQrScanError(true);
  568. } finally {
  569. setIsSubmittingAll(false);
  570. }
  571. }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign, onBack]);
  572. const scannedItemsCount = useMemo(() => {
  573. return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length;
  574. }, [combinedLotData]);
  575. // 修改:初始化时加载数据(移除 loadUnassignedOrders)
  576. useEffect(() => {
  577. if (session && currentUserId && !initializationRef.current) {
  578. console.log(" Session loaded, initializing job order...");
  579. initializationRef.current = true;
  580. fetchJobOrderData();
  581. }
  582. }, [session, currentUserId, fetchJobOrderData]);
  583. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  584. console.log(` Processing Second QR Code for lot: ${lotNo}`);
  585. if (processedQrCodes.has(lotNo)) {
  586. console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
  587. return;
  588. }
  589. const currentLotData = combinedLotData;
  590. const matchingLots = currentLotData.filter(lot =>
  591. lot.lotNo === lotNo ||
  592. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  593. );
  594. if (matchingLots.length === 0) {
  595. console.error(`❌ Lot not found: ${lotNo}`);
  596. setQrScanError(true);
  597. setQrScanSuccess(false);
  598. return;
  599. }
  600. try {
  601. let successCount = 0;
  602. for (const matchingLot of matchingLots) {
  603. const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
  604. if (processedQrCodes.has(itemKey)) {
  605. console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
  606. continue;
  607. }
  608. const result = await updateSecondQrScanStatus(
  609. matchingLot.pickOrderId,
  610. matchingLot.itemId,
  611. currentUserId !!,
  612. matchingLot.requiredQty || 1
  613. );
  614. console.log("result", result);
  615. console.log("currentUserId", currentUserId);
  616. if (result.code === "SUCCESS") {
  617. successCount++;
  618. setProcessedQrCodes(prev => new Set(prev).add(itemKey));
  619. console.log(` Second QR scan status updated for item ${matchingLot.itemId}`);
  620. } else {
  621. console.error(`❌ Failed to update second QR scan status: ${result.message}`);
  622. }
  623. }
  624. if (successCount > 0) {
  625. setQrScanSuccess(true);
  626. setQrScanError(false);
  627. setIsRefreshingData(true);
  628. await fetchJobOrderData();
  629. setTimeout(() => {
  630. setQrScanSuccess(false);
  631. setIsRefreshingData(false);
  632. }, 500);
  633. } else {
  634. setQrScanError(true);
  635. setQrScanSuccess(false);
  636. }
  637. } catch (error: any) {
  638. console.error("❌ Error processing second QR code:", error);
  639. const isAuthError = error?.status === 401 ||
  640. error?.status === 403 ||
  641. error?.message?.toLowerCase().includes('unauthorized') ||
  642. error?.message?.toLowerCase().includes('token');
  643. if (isAuthError) {
  644. console.log("🔒 Authentication error in QR submit, unassigning pick order");
  645. await handleUnassign(currentPickOrderId);
  646. }
  647. setQrScanError(true);
  648. setQrScanSuccess(false);
  649. }
  650. }, [combinedLotData, fetchJobOrderData, processedQrCodes, currentUserId, currentPickOrderId, handleUnassign]);
  651. useEffect(() => {
  652. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  653. return;
  654. }
  655. const latestQr = qrValues[qrValues.length - 1];
  656. if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
  657. console.log("⏭️ QR code already processed, skipping...");
  658. return;
  659. }
  660. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  661. setLastProcessedQr(latestQr);
  662. let lotNo = '';
  663. try {
  664. const qrData = JSON.parse(latestQr);
  665. if (qrData.stockInLineId && qrData.itemId) {
  666. fetchStockInLineInfo(qrData.stockInLineId)
  667. .then((stockInLineInfo) => {
  668. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  669. const extractedLotNo = stockInLineInfo.lotNo;
  670. if (extractedLotNo) {
  671. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  672. handleQrCodeSubmit(extractedLotNo);
  673. }
  674. })
  675. .catch((error) => {
  676. console.error("Outside QR scan - Error fetching stock in line info:", error);
  677. });
  678. return;
  679. }
  680. } catch (error) {
  681. lotNo = latestQr.replace(/[{}]/g, '');
  682. }
  683. if (lotNo) {
  684. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  685. handleQrCodeSubmit(lotNo);
  686. }
  687. }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]);
  688. useEffect(() => {
  689. return () => {
  690. if (isManualScanning) {
  691. console.log("🧹 Second scan component unmounting, stopping QR scanner...");
  692. stopScan();
  693. resetScan();
  694. }
  695. };
  696. }, [isManualScanning, stopScan, resetScan]);
  697. const handleManualInputSubmit = useCallback(() => {
  698. if (qrScanInput.trim() !== '') {
  699. handleQrCodeSubmit(qrScanInput.trim());
  700. }
  701. }, [qrScanInput, handleQrCodeSubmit]);
  702. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  703. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  704. console.log(` QR Code verified for lot: ${lotNo}`);
  705. const requiredQty = selectedLotForQr.requiredQty;
  706. const lotId = selectedLotForQr.lotId;
  707. const stockOutLineData: CreateStockOutLine = {
  708. consoCode: selectedLotForQr.pickOrderConsoCode,
  709. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  710. inventoryLotLineId: selectedLotForQr.lotId,
  711. qty: 0.0
  712. };
  713. try {
  714. setQrModalOpen(false);
  715. setSelectedLotForQr(null);
  716. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  717. setTimeout(() => {
  718. setPickQtyData(prev => ({
  719. ...prev,
  720. [lotKey]: requiredQty
  721. }));
  722. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  723. }, 500);
  724. await fetchJobOrderData();
  725. } catch (error) {
  726. console.error("Error creating stock out line:", error);
  727. }
  728. }
  729. }, [selectedLotForQr, fetchJobOrderData]);
  730. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  731. if (value === '' || value === null || value === undefined) {
  732. setPickQtyData(prev => ({
  733. ...prev,
  734. [lotKey]: 0
  735. }));
  736. return;
  737. }
  738. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  739. if (isNaN(numericValue)) {
  740. setPickQtyData(prev => ({
  741. ...prev,
  742. [lotKey]: 0
  743. }));
  744. return;
  745. }
  746. setPickQtyData(prev => ({
  747. ...prev,
  748. [lotKey]: numericValue
  749. }));
  750. }, []);
  751. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  752. try {
  753. const result = await submitSecondScanQuantity(
  754. lot.pickOrderId,
  755. lot.itemId,
  756. {
  757. qty: submitQty,
  758. isMissing: false,
  759. isBad: false,
  760. reason: undefined,
  761. userId: currentUserId ?? 0
  762. }
  763. );
  764. if (result.code === "SUCCESS") {
  765. console.log(` Second scan quantity submitted: ${submitQty}`);
  766. await fetchJobOrderData();
  767. } else {
  768. console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
  769. }
  770. } catch (error) {
  771. console.error("Error submitting second scan quantity:", error);
  772. }
  773. }, [fetchJobOrderData, currentUserId]);
  774. const handlePickExecutionForm = useCallback((lot: any) => {
  775. console.log("=== Pick Execution Form ===");
  776. console.log("Lot data:", lot);
  777. if (!lot) {
  778. console.warn("No lot data provided for pick execution form");
  779. return;
  780. }
  781. console.log("Opening pick execution form for lot:", lot.lotNo);
  782. setSelectedLotForExecutionForm(lot);
  783. setPickExecutionFormOpen(true);
  784. console.log("Pick execution form opened for lot ID:", lot.lotId);
  785. }, []);
  786. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  787. try {
  788. console.log("Pick execution form submitted:", data);
  789. if (!currentUserId) {
  790. console.error("❌ No current user ID available");
  791. return;
  792. }
  793. const result = await recordSecondScanIssue(
  794. selectedLotForExecutionForm.pickOrderId,
  795. selectedLotForExecutionForm.itemId,
  796. {
  797. qty: data.actualPickQty,
  798. missQty: data.missQty || 0,
  799. badItemQty: data.badItemQty || 0,
  800. isMissing: data.missQty > 0,
  801. isBad: data.badItemQty > 0,
  802. reason: data.issueRemark || '',
  803. createdBy: currentUserId,
  804. type: "match"
  805. }
  806. );
  807. console.log("Pick execution issue recorded:", result);
  808. if (result && result.code === "SUCCESS") {
  809. console.log(" Pick execution issue recorded successfully");
  810. } else {
  811. console.error("❌ Failed to record pick execution issue:", result);
  812. }
  813. setPickExecutionFormOpen(false);
  814. setSelectedLotForExecutionForm(null);
  815. await fetchJobOrderData();
  816. } catch (error) {
  817. console.error("Error submitting pick execution form:", error);
  818. }
  819. }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData]);
  820. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  821. const requiredQty = lot.requiredQty || 0;
  822. const stockOutLineQty = lot.stockOutLineQty || 0;
  823. return Math.max(0, requiredQty - stockOutLineQty);
  824. }, []);
  825. const searchCriteria: Criterion<any>[] = [
  826. {
  827. label: t("Pick Order Code"),
  828. paramName: "pickOrderCode",
  829. type: "text",
  830. },
  831. {
  832. label: t("Item Code"),
  833. paramName: "itemCode",
  834. type: "text",
  835. },
  836. {
  837. label: t("Item Name"),
  838. paramName: "itemName",
  839. type: "text",
  840. },
  841. {
  842. label: t("Lot No"),
  843. paramName: "lotNo",
  844. type: "text",
  845. },
  846. ];
  847. const handleSearch = useCallback((query: Record<string, any>) => {
  848. setSearchQuery({ ...query });
  849. console.log("Search query:", query);
  850. if (!originalCombinedData) return;
  851. const filtered = originalCombinedData.filter((lot: any) => {
  852. const pickOrderCodeMatch = !query.pickOrderCode ||
  853. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  854. const itemCodeMatch = !query.itemCode ||
  855. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  856. const itemNameMatch = !query.itemName ||
  857. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  858. const lotNoMatch = !query.lotNo ||
  859. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  860. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  861. });
  862. setCombinedLotData(filtered);
  863. console.log("Filtered lots count:", filtered.length);
  864. }, [originalCombinedData]);
  865. const handleReset = useCallback(() => {
  866. setSearchQuery({});
  867. if (originalCombinedData) {
  868. setCombinedLotData(originalCombinedData);
  869. }
  870. }, [originalCombinedData]);
  871. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  872. setPaginationController(prev => ({
  873. ...prev,
  874. pageNum: newPage,
  875. }));
  876. }, []);
  877. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  878. const newPageSize = parseInt(event.target.value, 10);
  879. setPaginationController({
  880. pageNum: 0,
  881. pageSize: newPageSize,
  882. });
  883. }, []);
  884. const paginatedData = useMemo(() => {
  885. const sortedData = [...combinedLotData].sort((a, b) => {
  886. const aIndex = a.routerIndex || 0;
  887. const bIndex = b.routerIndex || 0;
  888. if (aIndex !== bIndex) {
  889. return aIndex - bIndex;
  890. }
  891. if (a.pickOrderCode !== b.pickOrderCode) {
  892. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  893. }
  894. return (a.lotNo || '').localeCompare(b.lotNo || '');
  895. });
  896. const startIndex = paginationController.pageNum * paginationController.pageSize;
  897. const endIndex = startIndex + paginationController.pageSize;
  898. return sortedData.slice(startIndex, endIndex);
  899. }, [combinedLotData, paginationController]);
  900. const handleStartScan = useCallback(() => {
  901. console.log(" Starting manual QR scan...");
  902. setIsManualScanning(true);
  903. setProcessedQrCodes(new Set());
  904. setLastProcessedQr('');
  905. setQrScanError(false);
  906. setQrScanSuccess(false);
  907. startScan();
  908. }, [startScan]);
  909. const handleStopScan = useCallback(() => {
  910. console.log("⏹️ Stopping manual QR scan...");
  911. setIsManualScanning(false);
  912. setQrScanError(false);
  913. setQrScanSuccess(false);
  914. stopScan();
  915. resetScan();
  916. }, [stopScan, resetScan]);
  917. useEffect(() => {
  918. if (isManualScanning && combinedLotData.length === 0) {
  919. console.log("⏹️ No data available, auto-stopping QR scan...");
  920. handleStopScan();
  921. }
  922. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  923. const getStatusMessage = useCallback((lot: any) => {
  924. switch (lot.stockOutLineStatus?.toLowerCase()) {
  925. case 'pending':
  926. return t("Please finish QR code scan and pick order.");
  927. case 'checked':
  928. return t("Please submit the pick order.");
  929. case 'partially_completed':
  930. return t("Partial quantity submitted. Please submit more or complete the order.");
  931. case 'completed':
  932. return t("Pick order completed successfully!");
  933. case 'rejected':
  934. return t("Lot has been rejected and marked as unavailable.");
  935. case 'unavailable':
  936. return t("This order is insufficient, please pick another lot.");
  937. default:
  938. return t("Please finish QR code scan and pick order.");
  939. }
  940. }, [t]);
  941. return (
  942. <TestQrCodeProvider
  943. lotData={combinedLotData}
  944. onScanLot={handleQrCodeSubmit}
  945. filterActive={(lot) => (
  946. lot.matchStatus !== 'completed' &&
  947. lot.lotAvailability !== 'rejected'
  948. )}
  949. >
  950. <FormProvider {...formProps}>
  951. <Stack spacing={2}>
  952. {/* 添加返回按钮 */}
  953. {onBack && (
  954. <Box>
  955. <Button
  956. variant="outlined"
  957. onClick={handleBack}
  958. startIcon={<ArrowBackIcon />}
  959. >
  960. {t("Back to List")}
  961. </Button>
  962. </Box>
  963. )}
  964. {/* Job Order Header */}
  965. {jobOrderData && (
  966. <Paper sx={{ p: 2 }}>
  967. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  968. <Typography variant="subtitle1">
  969. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  970. </Typography>
  971. <Typography variant="subtitle1">
  972. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  973. </Typography>
  974. <Typography variant="subtitle1">
  975. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  976. </Typography>
  977. </Stack>
  978. </Paper>
  979. )}
  980. {/* Combined Lot Table */}
  981. <Box>
  982. <Button
  983. variant="contained"
  984. color="success"
  985. onClick={handleSubmitAllScanned}
  986. disabled={isSubmittingAll}
  987. sx={{ minWidth: '160px' }}
  988. >
  989. {isSubmittingAll ? (
  990. <>
  991. <CircularProgress size={16} sx={{ mr: 1 }} />
  992. {t("Submitting...")}
  993. </>
  994. ) : (
  995. t("Confirm All")
  996. )}
  997. </Button>
  998. {/*
  999. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1000. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1001. {!isManualScanning ? (
  1002. <Button
  1003. variant="contained"
  1004. startIcon={<QrCodeIcon />}
  1005. onClick={handleStartScan}
  1006. color="primary"
  1007. sx={{ minWidth: '120px' }}
  1008. >
  1009. {t("Start QR Scan")}
  1010. </Button>
  1011. ) : (
  1012. <Button
  1013. variant="outlined"
  1014. startIcon={<QrCodeIcon />}
  1015. onClick={handleStopScan}
  1016. color="secondary"
  1017. sx={{ minWidth: '120px' }}
  1018. >
  1019. {t("Stop QR Scan")}
  1020. </Button>
  1021. )}
  1022. <Button
  1023. variant="contained"
  1024. color="success"
  1025. onClick={handleSubmitAllScanned}
  1026. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1027. sx={{ minWidth: '160px' }}
  1028. >
  1029. {isSubmittingAll ? (
  1030. <>
  1031. <CircularProgress size={16} sx={{ mr: 1 }} />
  1032. {t("Submitting...")}
  1033. </>
  1034. ) : (
  1035. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1036. )}
  1037. </Button>
  1038. </Box>
  1039. </Box>
  1040. {qrScanError && !qrScanSuccess && (
  1041. <Alert severity="error" sx={{ mb: 2 }}>
  1042. {t("QR code does not match any item in current orders.")}
  1043. </Alert>
  1044. )}
  1045. {qrScanSuccess && (
  1046. <Alert severity="success" sx={{ mb: 2 }}>
  1047. {t("QR code verified.")}
  1048. </Alert>
  1049. )}
  1050. */}
  1051. <TableContainer component={Paper}>
  1052. <Table>
  1053. <TableHead>
  1054. <TableRow>
  1055. <TableCell>{t("Index")}</TableCell>
  1056. <TableCell>{t("Route")}</TableCell>
  1057. <TableCell>{t("Handler")}</TableCell>
  1058. <TableCell>{t("Item Code")}</TableCell>
  1059. <TableCell>{t("Item Name")}</TableCell>
  1060. <TableCell>{t("Lot No")}</TableCell>
  1061. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  1062. {/* <TableCell align="center">{t("Scan Result")}</TableCell> */}
  1063. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  1064. </TableRow>
  1065. </TableHead>
  1066. <TableBody>
  1067. {paginatedData.length === 0 ? (
  1068. <TableRow>
  1069. <TableCell colSpan={8} align="center">
  1070. <Typography variant="body2" color="text.secondary">
  1071. {t("No data available")}
  1072. </Typography>
  1073. </TableCell>
  1074. </TableRow>
  1075. ) : (
  1076. paginatedData.map((lot, index) => (
  1077. <TableRow
  1078. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  1079. sx={{
  1080. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  1081. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  1082. '& .MuiTableCell-root': {
  1083. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  1084. }
  1085. }}
  1086. >
  1087. <TableCell>
  1088. <Typography variant="body2" fontWeight="bold">
  1089. {index + 1}
  1090. </Typography>
  1091. </TableCell>
  1092. <TableCell>
  1093. <Typography variant="body2">
  1094. {lot.routerRoute || '-'}
  1095. </Typography>
  1096. </TableCell>
  1097. <TableCell>{lot.handler || '-'}</TableCell>
  1098. <TableCell>{lot.itemCode}</TableCell>
  1099. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  1100. <TableCell>
  1101. <Box>
  1102. <Typography
  1103. sx={{
  1104. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  1105. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  1106. }}
  1107. >
  1108. {lot.lotNo}
  1109. </Typography>
  1110. </Box>
  1111. </TableCell>
  1112. <TableCell align="right">
  1113. {(() => {
  1114. const requiredQty = lot.requiredQty || 0;
  1115. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  1116. })()}
  1117. </TableCell>
  1118. <TableCell align="center">
  1119. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  1120. <Stack direction="row" spacing={1} alignItems="center">
  1121. <Button
  1122. variant="contained"
  1123. onClick={async () => {
  1124. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1125. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1126. handlePickQtyChange(lotKey, submitQty);
  1127. // 先更新 matching 狀態(可選,依你後端流程)
  1128. await updateSecondQrScanStatus(lot.pickOrderId, lot.itemId, currentUserId || 0, submitQty);
  1129. // 再提交數量並 await refetch,表格會即時更新提料員
  1130. await handleSubmitPickQtyWithQty(lot, submitQty);
  1131. }}
  1132. disabled={
  1133. lot.matchStatus === 'completed' ||
  1134. lot.matchStatus == 'scanned' ||
  1135. lot.lotAvailability === 'expired' ||
  1136. lot.lotAvailability === 'status_unavailable' ||
  1137. lot.lotAvailability === 'rejected'
  1138. }
  1139. sx={{
  1140. fontSize: '0.75rem',
  1141. py: 0.5,
  1142. minHeight: '28px',
  1143. minWidth: '70px'
  1144. }}
  1145. >
  1146. {t("Confirm")}
  1147. </Button>
  1148. <Button
  1149. variant="outlined"
  1150. size="small"
  1151. onClick={() => handlePickExecutionForm(lot)}
  1152. disabled={
  1153. lot.matchStatus === 'completed' ||
  1154. lot.matchStatus == 'scanned' ||
  1155. lot.lotAvailability === 'expired' ||
  1156. lot.lotAvailability === 'status_unavailable' ||
  1157. lot.lotAvailability === 'rejected'
  1158. }
  1159. sx={{
  1160. fontSize: '0.7rem',
  1161. py: 0.5,
  1162. minHeight: '28px',
  1163. minWidth: '60px',
  1164. borderColor: 'warning.main',
  1165. color: 'warning.main'
  1166. }}
  1167. title="Report missing or bad items"
  1168. >
  1169. {t("Issue")}
  1170. </Button>
  1171. </Stack>
  1172. </Box>
  1173. </TableCell>
  1174. </TableRow>
  1175. ))
  1176. )}
  1177. </TableBody>
  1178. </Table>
  1179. </TableContainer>
  1180. <TablePagination
  1181. component="div"
  1182. count={combinedLotData.length}
  1183. page={paginationController.pageNum}
  1184. rowsPerPage={paginationController.pageSize}
  1185. onPageChange={handlePageChange}
  1186. onRowsPerPageChange={handlePageSizeChange}
  1187. rowsPerPageOptions={[10, 25, 50]}
  1188. labelRowsPerPage={t("Rows per page")}
  1189. labelDisplayedRows={({ from, to, count }) =>
  1190. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1191. }
  1192. />
  1193. </Box>
  1194. </Stack>
  1195. {/* QR Code Modal */}
  1196. <QrCodeModal
  1197. open={qrModalOpen}
  1198. onClose={() => {
  1199. setQrModalOpen(false);
  1200. setSelectedLotForQr(null);
  1201. stopScan();
  1202. resetScan();
  1203. }}
  1204. lot={selectedLotForQr}
  1205. combinedLotData={combinedLotData}
  1206. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  1207. />
  1208. {/* Pick Execution Form Modal */}
  1209. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  1210. <GoodPickExecutionForm
  1211. open={pickExecutionFormOpen}
  1212. onClose={() => {
  1213. setPickExecutionFormOpen(false);
  1214. setSelectedLotForExecutionForm(null);
  1215. }}
  1216. onSubmit={handlePickExecutionFormSubmit}
  1217. selectedLot={selectedLotForExecutionForm}
  1218. selectedPickOrderLine={{
  1219. id: selectedLotForExecutionForm.pickOrderLineId,
  1220. itemId: selectedLotForExecutionForm.itemId,
  1221. itemCode: selectedLotForExecutionForm.itemCode,
  1222. itemName: selectedLotForExecutionForm.itemName,
  1223. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  1224. availableQty: selectedLotForExecutionForm.availableQty || 0,
  1225. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  1226. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  1227. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  1228. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  1229. suggestedList: [],
  1230. noLotLines: []
  1231. }}
  1232. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  1233. pickOrderCreateDate={new Date()}
  1234. />
  1235. )}
  1236. </FormProvider>
  1237. </TestQrCodeProvider>
  1238. );
  1239. };
  1240. export default JobPickExecution;