FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

542 lines
20 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Stack,
  6. Typography,
  7. Chip,
  8. CircularProgress,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableContainer,
  13. TableHead,
  14. TableRow,
  15. Paper,
  16. TextField,
  17. } from "@mui/material";
  18. import { useState, useCallback, useEffect, useRef } from "react";
  19. import { useTranslation } from "react-i18next";
  20. import {
  21. AllPickedStockTakeListReponse,
  22. getInventoryLotDetailsBySection,
  23. InventoryLotDetailResponse,
  24. saveStockTakeRecord,
  25. SaveStockTakeRecordRequest,
  26. BatchSaveStockTakeRecordRequest,
  27. batchSaveStockTakeRecords,
  28. } from "@/app/api/stockTake/actions";
  29. import { useSession } from "next-auth/react";
  30. import { SessionWithTokens } from "@/config/authConfig";
  31. import dayjs from "dayjs";
  32. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  33. interface PickerStockTakeProps {
  34. selectedSession: AllPickedStockTakeListReponse;
  35. onBack: () => void;
  36. onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
  37. }
  38. const PickerStockTake: React.FC<PickerStockTakeProps> = ({
  39. selectedSession,
  40. onBack,
  41. onSnackbar,
  42. }) => {
  43. const { t } = useTranslation(["inventory", "common"]);
  44. const { data: session } = useSession() as { data: SessionWithTokens | null };
  45. const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
  46. const [loadingDetails, setLoadingDetails] = useState(false);
  47. // 编辑状态
  48. const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null);
  49. const [firstQty, setFirstQty] = useState<string>("");
  50. const [secondQty, setSecondQty] = useState<string>("");
  51. const [firstBadQty, setFirstBadQty] = useState<string>("");
  52. const [secondBadQty, setSecondBadQty] = useState<string>("");
  53. const [remark, setRemark] = useState<string>("");
  54. const [saving, setSaving] = useState(false);
  55. const [batchSaving, setBatchSaving] = useState(false);
  56. const [shortcutInput, setShortcutInput] = useState<string>("");
  57. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  58. const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
  59. useEffect(() => {
  60. const loadDetails = async () => {
  61. setLoadingDetails(true);
  62. try {
  63. const details = await getInventoryLotDetailsBySection(
  64. selectedSession.stockTakeSession,
  65. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
  66. );
  67. setInventoryLotDetails(Array.isArray(details) ? details : []);
  68. } catch (e) {
  69. console.error(e);
  70. setInventoryLotDetails([]);
  71. } finally {
  72. setLoadingDetails(false);
  73. }
  74. };
  75. loadDetails();
  76. }, [selectedSession]);
  77. const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => {
  78. setEditingRecord(detail);
  79. setFirstQty(detail.firstStockTakeQty?.toString() || "");
  80. setSecondQty(detail.secondStockTakeQty?.toString() || "");
  81. setFirstBadQty(detail.firstBadQty?.toString() || "");
  82. setSecondBadQty(detail.secondBadQty?.toString() || "");
  83. setRemark(detail.remarks || "");
  84. }, []);
  85. const handleCancelEdit = useCallback(() => {
  86. setEditingRecord(null);
  87. setFirstQty("");
  88. setSecondQty("");
  89. setFirstBadQty("");
  90. setSecondBadQty("");
  91. setRemark("");
  92. }, []);
  93. const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
  94. if (!selectedSession || !currentUserId) {
  95. return;
  96. }
  97. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  98. const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  99. const qty = isFirstSubmit ? firstQty : secondQty;
  100. const badQty = isFirstSubmit ? firstBadQty : secondBadQty;
  101. if (!qty || !badQty) {
  102. onSnackbar(
  103. isFirstSubmit
  104. ? t("Please enter QTY and Bad QTY")
  105. : t("Please enter Second QTY and Bad QTY"),
  106. "error"
  107. );
  108. return;
  109. }
  110. setSaving(true);
  111. try {
  112. const request: SaveStockTakeRecordRequest = {
  113. stockTakeRecordId: detail.stockTakeRecordId || null,
  114. inventoryLotLineId: detail.id,
  115. qty: parseFloat(qty),
  116. badQty: parseFloat(badQty),
  117. remark: isSecondSubmit ? (remark || null) : null,
  118. };
  119. console.log('handleSaveStockTake: request:', request);
  120. console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId);
  121. console.log('handleSaveStockTake: currentUserId:', currentUserId);
  122. await saveStockTakeRecord(
  123. request,
  124. selectedSession.stockTakeId,
  125. currentUserId
  126. );
  127. onSnackbar(t("Stock take record saved successfully"), "success");
  128. handleCancelEdit();
  129. const details = await getInventoryLotDetailsBySection(
  130. selectedSession.stockTakeSession,
  131. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
  132. );
  133. setInventoryLotDetails(Array.isArray(details) ? details : []);
  134. } catch (e: any) {
  135. console.error("Save stock take record error:", e);
  136. let errorMessage = t("Failed to save stock take record");
  137. if (e?.message) {
  138. errorMessage = e.message;
  139. } else if (e?.response) {
  140. try {
  141. const errorData = await e.response.json();
  142. errorMessage = errorData.message || errorData.error || errorMessage;
  143. } catch {
  144. // ignore
  145. }
  146. }
  147. onSnackbar(errorMessage, "error");
  148. } finally {
  149. setSaving(false);
  150. }
  151. }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]);
  152. const handleBatchSubmitAll = useCallback(async () => {
  153. if (!selectedSession || !currentUserId) {
  154. console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId');
  155. return;
  156. }
  157. console.log('handleBatchSubmitAll: Starting batch save...');
  158. setBatchSaving(true);
  159. try {
  160. const request: BatchSaveStockTakeRecordRequest = {
  161. stockTakeId: selectedSession.stockTakeId,
  162. stockTakeSection: selectedSession.stockTakeSession,
  163. stockTakerId: currentUserId,
  164. };
  165. const result = await batchSaveStockTakeRecords(request);
  166. console.log('handleBatchSubmitAll: Result:', result);
  167. onSnackbar(
  168. t("Batch save completed: {{success}} success, {{errors}} errors", {
  169. success: result.successCount,
  170. errors: result.errorCount,
  171. }),
  172. result.errorCount > 0 ? "warning" : "success"
  173. );
  174. const details = await getInventoryLotDetailsBySection(
  175. selectedSession.stockTakeSession,
  176. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
  177. );
  178. setInventoryLotDetails(Array.isArray(details) ? details : []);
  179. } catch (e: any) {
  180. console.error("handleBatchSubmitAll: Error:", e);
  181. let errorMessage = t("Failed to batch save stock take records");
  182. if (e?.message) {
  183. errorMessage = e.message;
  184. } else if (e?.response) {
  185. try {
  186. const errorData = await e.response.json();
  187. errorMessage = errorData.message || errorData.error || errorMessage;
  188. } catch {
  189. // ignore
  190. }
  191. }
  192. onSnackbar(errorMessage, "error");
  193. } finally {
  194. setBatchSaving(false);
  195. }
  196. }, [selectedSession, t, currentUserId, onSnackbar]);
  197. useEffect(() => {
  198. handleBatchSubmitAllRef.current = handleBatchSubmitAll;
  199. }, [handleBatchSubmitAll]);
  200. useEffect(() => {
  201. const handleKeyPress = (e: KeyboardEvent) => {
  202. const target = e.target as HTMLElement;
  203. if (target && (
  204. target.tagName === 'INPUT' ||
  205. target.tagName === 'TEXTAREA' ||
  206. target.isContentEditable
  207. )) {
  208. return;
  209. }
  210. if (e.ctrlKey || e.metaKey || e.altKey) {
  211. return;
  212. }
  213. if (e.key.length === 1) {
  214. setShortcutInput(prev => {
  215. const newInput = prev + e.key;
  216. if (newInput === '{2fitestall}') {
  217. console.log('✅ Shortcut {2fitestall} detected!');
  218. setTimeout(() => {
  219. if (handleBatchSubmitAllRef.current) {
  220. console.log('Calling handleBatchSubmitAll...');
  221. handleBatchSubmitAllRef.current().catch(err => {
  222. console.error('Error in handleBatchSubmitAll:', err);
  223. });
  224. } else {
  225. console.error('handleBatchSubmitAllRef.current is null');
  226. }
  227. }, 0);
  228. return "";
  229. }
  230. if (newInput.length > 15) {
  231. return "";
  232. }
  233. if (newInput.length > 0 && !newInput.startsWith('{')) {
  234. return "";
  235. }
  236. if (newInput.length > 5 && !newInput.startsWith('{2fi')) {
  237. return "";
  238. }
  239. return newInput;
  240. });
  241. } else if (e.key === 'Backspace') {
  242. setShortcutInput(prev => prev.slice(0, -1));
  243. } else if (e.key === 'Escape') {
  244. setShortcutInput("");
  245. }
  246. };
  247. window.addEventListener('keydown', handleKeyPress);
  248. return () => {
  249. window.removeEventListener('keydown', handleKeyPress);
  250. };
  251. }, []);
  252. const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
  253. if (detail.stockTakeRecordStatus === "pass") {
  254. return true;
  255. }
  256. return false;
  257. }, []);
  258. return (
  259. <Box>
  260. <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
  261. {t("Back to List")}
  262. </Button>
  263. <Typography variant="h6" sx={{ mb: 2 }}>
  264. {t("Stock Take Section")}: {selectedSession.stockTakeSession}
  265. </Typography>
  266. {/*
  267. {shortcutInput && (
  268. <Box sx={{ mb: 2, p: 1.5, bgcolor: 'info.light', borderRadius: 1, border: '1px solid', borderColor: 'info.main' }}>
  269. <Typography variant="body2" color="info.dark" fontWeight={500}>
  270. {t("Shortcut Input")}: <strong style={{ fontFamily: 'monospace', fontSize: '1.1em' }}>{shortcutInput}</strong>
  271. </Typography>
  272. </Box>
  273. )}
  274. */}
  275. {loadingDetails ? (
  276. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  277. <CircularProgress />
  278. </Box>
  279. ) : (
  280. <TableContainer component={Paper}>
  281. <Table>
  282. <TableHead>
  283. <TableRow>
  284. <TableCell>{t("Warehouse Location")}</TableCell>
  285. <TableCell>{t("Item")}</TableCell>
  286. {/*<TableCell>{t("Item Name")}</TableCell>*/}
  287. {/*<TableCell>{t("Lot No")}</TableCell>*/}
  288. <TableCell>{t("Expiry Date")}</TableCell>
  289. <TableCell>{t("Qty")}</TableCell>
  290. <TableCell>{t("Bad Qty")}</TableCell>
  291. {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
  292. <TableCell>{t("Remark")}</TableCell>
  293. <TableCell>{t("UOM")}</TableCell>
  294. <TableCell>{t("Status")}</TableCell>
  295. <TableCell>{t("Record Status")}</TableCell>
  296. <TableCell>{t("Action")}</TableCell>
  297. </TableRow>
  298. </TableHead>
  299. <TableBody>
  300. {inventoryLotDetails.length === 0 ? (
  301. <TableRow>
  302. <TableCell colSpan={12} align="center">
  303. <Typography variant="body2" color="text.secondary">
  304. {t("No data")}
  305. </Typography>
  306. </TableCell>
  307. </TableRow>
  308. ) : (
  309. inventoryLotDetails.map((detail) => {
  310. const isEditing = editingRecord?.id === detail.id;
  311. const submitDisabled = isSubmitDisabled(detail);
  312. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  313. const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  314. return (
  315. <TableRow key={detail.id}>
  316. <TableCell>{detail.warehouseCode || "-"}</TableCell>
  317. <TableCell sx={{
  318. maxWidth: 100,
  319. wordBreak: 'break-word',
  320. whiteSpace: 'normal',
  321. lineHeight: 1.5
  322. }}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell>
  323. {/*
  324. <TableCell
  325. sx={{
  326. maxWidth: 200,
  327. wordBreak: 'break-word',
  328. whiteSpace: 'normal',
  329. lineHeight: 1.5
  330. }}
  331. >
  332. {detail.itemName || "-"}
  333. </TableCell>*/}
  334. {/*<TableCell>{detail.lotNo || "-"}</TableCell>*/}
  335. <TableCell>
  336. {detail.expiryDate
  337. ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
  338. : "-"}
  339. </TableCell>
  340. <TableCell>
  341. <Stack spacing={0.5}>
  342. {isEditing && isFirstSubmit ? (
  343. <TextField
  344. size="small"
  345. type="number"
  346. value={firstQty}
  347. onChange={(e) => setFirstQty(e.target.value)}
  348. sx={{ width: 100 }}
  349. />
  350. ) : detail.firstStockTakeQty ? (
  351. <Typography variant="body2">
  352. {t("First")}: {detail.firstStockTakeQty.toFixed(2)}
  353. </Typography>
  354. ) : null}
  355. {isEditing && isSecondSubmit ? (
  356. <TextField
  357. size="small"
  358. type="number"
  359. value={secondQty}
  360. onChange={(e) => setSecondQty(e.target.value)}
  361. sx={{ width: 100 }}
  362. />
  363. ) : detail.secondStockTakeQty ? (
  364. <Typography variant="body2">
  365. {t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
  366. </Typography>
  367. ) : null}
  368. {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && (
  369. <Typography variant="body2" color="text.secondary">
  370. -
  371. </Typography>
  372. )}
  373. </Stack>
  374. </TableCell>
  375. <TableCell>
  376. <Stack spacing={0.5}>
  377. {isEditing && isFirstSubmit ? (
  378. <TextField
  379. size="small"
  380. type="number"
  381. value={firstBadQty}
  382. onChange={(e) => setFirstBadQty(e.target.value)}
  383. sx={{ width: 100 }}
  384. />
  385. ) : detail.firstBadQty ? (
  386. <Typography variant="body2">
  387. {t("First")}: {detail.firstBadQty.toFixed(2)}
  388. </Typography>
  389. ) : null}
  390. {isEditing && isSecondSubmit ? (
  391. <TextField
  392. size="small"
  393. type="number"
  394. value={secondBadQty}
  395. onChange={(e) => setSecondBadQty(e.target.value)}
  396. sx={{ width: 100 }}
  397. />
  398. ) : detail.secondBadQty ? (
  399. <Typography variant="body2">
  400. {t("Second")}: {detail.secondBadQty.toFixed(2)}
  401. </Typography>
  402. ) : null}
  403. {!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
  404. <Typography variant="body2" color="text.secondary">
  405. -
  406. </Typography>
  407. )}
  408. </Stack>
  409. </TableCell>
  410. <TableCell sx={{ width: 180 }}>
  411. {isEditing && isSecondSubmit ? (
  412. <>
  413. <Typography variant="body2">{t("Remark")}</Typography>
  414. <TextField
  415. size="small"
  416. value={remark}
  417. onChange={(e) => setRemark(e.target.value)}
  418. sx={{ width: 150 }}
  419. // If you want a single-line input, remove multiline/rows:
  420. // multiline
  421. // rows={2}
  422. />
  423. </>
  424. ) : (
  425. <Typography variant="body2">
  426. {detail.remarks || "-"}
  427. </Typography>
  428. )}
  429. </TableCell>
  430. <TableCell>{detail.uom || "-"}</TableCell>
  431. <TableCell>
  432. {detail.status ? (
  433. <Chip size="small" label={t(detail.status)} color="default" />
  434. ) : (
  435. "-"
  436. )}
  437. </TableCell>
  438. <TableCell>
  439. {detail.stockTakeRecordStatus === "pass" ? (
  440. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
  441. ) : detail.stockTakeRecordStatus === "notMatch" ? (
  442. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
  443. ) : (
  444. <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
  445. )}
  446. </TableCell>
  447. <TableCell>
  448. {isEditing ? (
  449. <Stack direction="row" spacing={1}>
  450. <Button
  451. size="small"
  452. variant="contained"
  453. onClick={() => handleSaveStockTake(detail)}
  454. disabled={saving || submitDisabled}
  455. >
  456. {t("Save")}
  457. </Button>
  458. <Button
  459. size="small"
  460. onClick={handleCancelEdit}
  461. >
  462. {t("Cancel")}
  463. </Button>
  464. </Stack>
  465. ) : (
  466. <Button
  467. size="small"
  468. variant="outlined"
  469. onClick={() => handleStartEdit(detail)}
  470. disabled={submitDisabled}
  471. >
  472. {!detail.stockTakeRecordId
  473. ? t("Input")
  474. : detail.stockTakeRecordStatus === "notMatch"
  475. ? t("Input")
  476. : t("View")}
  477. </Button>
  478. )}
  479. </TableCell>
  480. </TableRow>
  481. );
  482. })
  483. )}
  484. </TableBody>
  485. </Table>
  486. </TableContainer>
  487. )}
  488. </Box>
  489. );
  490. };
  491. export default PickerStockTake;