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

PickerStockTake.tsx 33 KiB

3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1日前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2週間前
2週間前
2週間前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  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. TablePagination,
  18. Select, // Add this
  19. MenuItem, // Add this
  20. FormControl, // Add this
  21. InputLabel,
  22. } from "@mui/material";
  23. import { SelectChangeEvent } from "@mui/material/Select";
  24. import { useState, useCallback, useEffect, useRef } from "react";
  25. import { useTranslation } from "react-i18next";
  26. import {
  27. AllPickedStockTakeListReponse,
  28. getInventoryLotDetailsBySection,
  29. InventoryLotDetailResponse,
  30. saveStockTakeRecord,
  31. SaveStockTakeRecordRequest,
  32. BatchSaveStockTakeRecordRequest,
  33. batchSaveStockTakeRecords,
  34. } from "@/app/api/stockTake/actions";
  35. import { useSession } from "next-auth/react";
  36. import { SessionWithTokens } from "@/config/authConfig";
  37. import dayjs from "dayjs";
  38. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  39. interface PickerStockTakeProps {
  40. selectedSession: AllPickedStockTakeListReponse;
  41. onBack: () => void;
  42. onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
  43. }
  44. const PickerStockTake: React.FC<PickerStockTakeProps> = ({
  45. selectedSession,
  46. onBack,
  47. onSnackbar,
  48. }) => {
  49. const { t } = useTranslation(["inventory", "common"]);
  50. const { data: session } = useSession() as { data: SessionWithTokens | null };
  51. const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
  52. const [loadingDetails, setLoadingDetails] = useState(false);
  53. const [recordInputs, setRecordInputs] = useState<Record<number, {
  54. firstQty: string;
  55. secondQty: string;
  56. firstBadQty: string;
  57. secondBadQty: string;
  58. remark: string;
  59. }>>({});
  60. const [savingRecordId, setSavingRecordId] = useState<number | null>(null);
  61. const [remark, setRemark] = useState<string>("");
  62. const [saving, setSaving] = useState(false);
  63. const [batchSaving, setBatchSaving] = useState(false);
  64. const [shortcutInput, setShortcutInput] = useState<string>("");
  65. const [page, setPage] = useState(0);
  66. const [pageSize, setPageSize] = useState<number | string>("all");
  67. const [total, setTotal] = useState(0);
  68. const totalPages = pageSize === "all" ? 1 : Math.ceil(total / (pageSize as number));
  69. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  70. const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
  71. const handleChangePage = useCallback((event: unknown, newPage: number) => {
  72. setPage(newPage);
  73. }, []);
  74. const handlePageSelectChange = useCallback((event: SelectChangeEvent<number>) => {
  75. const newPage = parseInt(event.target.value as string, 10) - 1; // Convert to 0-indexed
  76. setPage(Math.max(0, Math.min(newPage, totalPages - 1)));
  77. }, [totalPages]);
  78. const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  79. const newSize = parseInt(event.target.value, 10);
  80. if (newSize === -1) {
  81. setPageSize("all");
  82. } else if (!isNaN(newSize)) {
  83. setPageSize(newSize);
  84. }
  85. setPage(0);
  86. }, []);
  87. const loadDetails = useCallback(async (
  88. pageNum: number,
  89. size: number | string,
  90. options?: { silent?: boolean }
  91. ) => {
  92. console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber });
  93. setLoadingDetails(true);
  94. try {
  95. let actualSize: number;
  96. if (size === "all") {
  97. // Use totalInventoryLotNumber from selectedSession if available
  98. if (selectedSession.totalInventoryLotNumber > 0) {
  99. actualSize = selectedSession.totalInventoryLotNumber;
  100. console.log('Using "all" - actualSize set to totalInventoryLotNumber:', actualSize);
  101. } else if (total > 0) {
  102. // Fallback to total from previous response
  103. actualSize = total;
  104. console.log('Using "all" - actualSize set to total from state:', actualSize);
  105. } else {
  106. // Last resort: use a large number
  107. actualSize = 10000;
  108. console.log('Using "all" - actualSize set to default 10000');
  109. }
  110. } else {
  111. actualSize = typeof size === 'string' ? parseInt(size, 10) : size;
  112. console.log('Using specific size - actualSize set to:', actualSize);
  113. }
  114. console.log('Calling getInventoryLotDetailsBySection with actualSize:', actualSize);
  115. const response = await getInventoryLotDetailsBySection(
  116. selectedSession.stockTakeSession,
  117. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
  118. pageNum,
  119. actualSize,
  120. selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
  121. ? selectedSession.stockTakeRoundId
  122. : null
  123. );
  124. setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
  125. setTotal(response.total || 0);
  126. } catch (e) {
  127. console.error(e);
  128. setInventoryLotDetails([]);
  129. setTotal(0);
  130. } finally {
  131. setLoadingDetails(false);
  132. }
  133. }, [selectedSession, total]);
  134. useEffect(() => {
  135. setRecordInputs((prev) => {
  136. const next: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {};
  137. inventoryLotDetails.forEach((detail) => {
  138. const hasServerFirst = detail.firstStockTakeQty != null;
  139. const hasServerSecond = detail.secondStockTakeQty != null;
  140. const firstTotal = hasServerFirst
  141. ? (detail.firstStockTakeQty! + (detail.firstBadQty ?? 0)).toString()
  142. : "";
  143. const secondTotal = hasServerSecond
  144. ? (detail.secondStockTakeQty! + (detail.secondBadQty ?? 0)).toString()
  145. : "";
  146. const existing = prev[detail.id];
  147. next[detail.id] = {
  148. firstQty: hasServerFirst ? firstTotal : (existing?.firstQty ?? firstTotal),
  149. secondQty: hasServerSecond ? secondTotal : (existing?.secondQty ?? secondTotal),
  150. firstBadQty: hasServerFirst ? (detail.firstBadQty?.toString() || "") : (existing?.firstBadQty ?? ""),
  151. secondBadQty: hasServerSecond ? (detail.secondBadQty?.toString() || "") : (existing?.secondBadQty ?? ""),
  152. remark: hasServerSecond ? (detail.remarks || "") : (existing?.remark ?? detail.remarks ?? ""),
  153. };
  154. });
  155. return next;
  156. });
  157. }, [inventoryLotDetails]);
  158. useEffect(() => {
  159. loadDetails(page, pageSize);
  160. }, [page, pageSize, loadDetails]);
  161. const formatNumber = (num: number | null | undefined): string => {
  162. if (num == null || Number.isNaN(num)) return "0";
  163. return num.toLocaleString("en-US", {
  164. minimumFractionDigits: 0,
  165. maximumFractionDigits: 0,
  166. });
  167. };
  168. const handleSaveStockTake = useCallback(
  169. async (detail: InventoryLotDetailResponse) => {
  170. if (!selectedSession || !currentUserId) {
  171. return;
  172. }
  173. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  174. const isSecondSubmit =
  175. detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  176. // 现在用户输入的是 total 和 bad,需要算 available = total - bad
  177. const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
  178. const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty;
  179. // 只檢查 totalQty,Bad Qty 未輸入時預設為 0
  180. if (!totalQtyStr) {
  181. onSnackbar(
  182. isFirstSubmit
  183. ? t("Please enter QTY")
  184. : t("Please enter Second QTY"),
  185. "error"
  186. );
  187. return;
  188. }
  189. const totalQty = parseFloat(totalQtyStr);
  190. const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0
  191. if (Number.isNaN(totalQty)) {
  192. onSnackbar(t("Invalid QTY"), "error");
  193. return;
  194. }
  195. const availableQty = totalQty - badQty;
  196. if (availableQty < 0) {
  197. onSnackbar(t("Available QTY cannot be negative"), "error");
  198. return;
  199. }
  200. setSaving(true);
  201. try {
  202. const request: SaveStockTakeRecordRequest = {
  203. stockTakeRecordId: detail.stockTakeRecordId || null,
  204. inventoryLotLineId: detail.id,
  205. qty: availableQty, // 保存 available qty
  206. badQty: badQty, // 保存 bad qty
  207. remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null,
  208. };
  209. console.log("handleSaveStockTake: request:", request);
  210. console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId);
  211. console.log("handleSaveStockTake: currentUserId:", currentUserId);
  212. await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId);
  213. onSnackbar(t("Stock take record saved successfully"), "success");
  214. //await loadDetails(page, pageSize, { silent: true });
  215. setInventoryLotDetails((prev) =>
  216. prev.map((d) =>
  217. d.id === detail.id
  218. ? {
  219. ...d,
  220. stockTakeRecordId: d.stockTakeRecordId ?? null, // 首次儲存後可從 response 取得,此處先保留
  221. firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty,
  222. firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null,
  223. secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty,
  224. secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null,
  225. remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks,
  226. stockTakeRecordStatus: "pass",
  227. }
  228. : d
  229. )
  230. );
  231. } catch (e: any) {
  232. console.error("Save stock take record error:", e);
  233. let errorMessage = t("Failed to save stock take record");
  234. if (e?.message) {
  235. errorMessage = e.message;
  236. } else if (e?.response) {
  237. try {
  238. const errorData = await e.response.json();
  239. errorMessage = errorData.message || errorData.error || errorMessage;
  240. } catch {
  241. // ignore
  242. }
  243. }
  244. onSnackbar(errorMessage, "error");
  245. } finally {
  246. setSaving(false);
  247. }
  248. },
  249. [
  250. selectedSession,
  251. recordInputs,
  252. remark,
  253. t,
  254. currentUserId,
  255. onSnackbar,
  256. ]
  257. );
  258. const handleBatchSubmitAll = useCallback(
  259. async () => {
  260. if (!selectedSession || !currentUserId) {
  261. console.log("handleBatchSubmitAll: Missing selectedSession or currentUserId");
  262. return;
  263. }
  264. console.log("handleBatchSubmitAll: Starting batch save...");
  265. setBatchSaving(true);
  266. try {
  267. const request: BatchSaveStockTakeRecordRequest = {
  268. stockTakeId: selectedSession.stockTakeId,
  269. stockTakeSection: selectedSession.stockTakeSession,
  270. stockTakerId: currentUserId,
  271. };
  272. const result = await batchSaveStockTakeRecords(request);
  273. console.log("handleBatchSubmitAll: Result:", result);
  274. onSnackbar(
  275. t("Batch save completed: {{success}} success, {{errors}} errors", {
  276. success: result.successCount,
  277. errors: result.errorCount,
  278. }),
  279. result.errorCount > 0 ? "warning" : "success"
  280. );
  281. await loadDetails(page, pageSize);
  282. } catch (e: any) {
  283. console.error("handleBatchSubmitAll: Error:", e);
  284. let errorMessage = t("Failed to batch save stock take records");
  285. if (e?.message) {
  286. errorMessage = e.message;
  287. } else if (e?.response) {
  288. try {
  289. const errorData = await e.response.json();
  290. errorMessage = errorData.message || errorData.error || errorMessage;
  291. } catch {
  292. // ignore
  293. }
  294. }
  295. onSnackbar(errorMessage, "error");
  296. } finally {
  297. setBatchSaving(false);
  298. }
  299. },
  300. [selectedSession, t, currentUserId, onSnackbar]
  301. );
  302. useEffect(() => {
  303. handleBatchSubmitAllRef.current = handleBatchSubmitAll;
  304. }, [handleBatchSubmitAll]);
  305. useEffect(() => {
  306. const handleKeyPress = (e: KeyboardEvent) => {
  307. const target = e.target as HTMLElement;
  308. if (
  309. target &&
  310. (target.tagName === "INPUT" ||
  311. target.tagName === "TEXTAREA" ||
  312. target.isContentEditable)
  313. ) {
  314. return;
  315. }
  316. if (e.ctrlKey || e.metaKey || e.altKey) {
  317. return;
  318. }
  319. if (e.key.length === 1) {
  320. setShortcutInput((prev) => {
  321. const newInput = prev + e.key;
  322. if (newInput === "{2fitestall}") {
  323. console.log("✅ Shortcut {2fitestall} detected!");
  324. setTimeout(() => {
  325. if (handleBatchSubmitAllRef.current) {
  326. console.log("Calling handleBatchSubmitAll...");
  327. handleBatchSubmitAllRef.current().catch((err) => {
  328. console.error("Error in handleBatchSubmitAll:", err);
  329. });
  330. } else {
  331. console.error("handleBatchSubmitAllRef.current is null");
  332. }
  333. }, 0);
  334. return "";
  335. }
  336. if (newInput.length > 15) {
  337. return "";
  338. }
  339. if (newInput.length > 0 && !newInput.startsWith("{")) {
  340. return "";
  341. }
  342. if (newInput.length > 5 && !newInput.startsWith("{2fi")) {
  343. return "";
  344. }
  345. return newInput;
  346. });
  347. } else if (e.key === "Backspace") {
  348. setShortcutInput((prev) => prev.slice(0, -1));
  349. } else if (e.key === "Escape") {
  350. setShortcutInput("");
  351. }
  352. };
  353. window.addEventListener("keydown", handleKeyPress);
  354. return () => {
  355. window.removeEventListener("keydown", handleKeyPress);
  356. };
  357. }, []);
  358. const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => {
  359. // 禁止小数点、逗号、科学计数、正负号
  360. if ([".", ",", "e", "E", "+", "-"].includes(e.key)) {
  361. e.preventDefault();
  362. }
  363. };
  364. const sanitizeIntegerInput = (value: string) => {
  365. // 只保留数字
  366. return value.replace(/[^\d]/g, "");
  367. };
  368. const isIntegerString = (value: string) => /^\d+$/.test(value);
  369. const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
  370. if (selectedSession?.status?.toLowerCase() === "completed") {
  371. return true;
  372. }
  373. const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
  374. if (recordStatus === "pass" || recordStatus === "completed") {
  375. return true;
  376. }
  377. return false;
  378. }, [selectedSession?.status]);
  379. const handleSubmitAllInputted = useCallback(async () => {
  380. if (!selectedSession || !currentUserId) return;
  381. const toSave = inventoryLotDetails.filter((detail) => {
  382. const submitDisabled = isSubmitDisabled(detail);
  383. if (submitDisabled) return false;
  384. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  385. const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  386. const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
  387. return !!totalQtyStr && totalQtyStr.trim() !== "";
  388. });
  389. if (toSave.length === 0) {
  390. onSnackbar(t("No valid input to submit"), "warning");
  391. return;
  392. }
  393. setBatchSaving(true);
  394. let successCount = 0;
  395. let errorCount = 0;
  396. for (const detail of toSave) {
  397. try {
  398. await handleSaveStockTake(detail);
  399. successCount++;
  400. } catch {
  401. errorCount++;
  402. }
  403. }
  404. setBatchSaving(false);
  405. onSnackbar(
  406. t("Submit completed: {{success}} success, {{errors}} errors", { success: successCount, errors: errorCount }),
  407. errorCount > 0 ? "warning" : "success"
  408. );
  409. }, [inventoryLotDetails, recordInputs, isSubmitDisabled, handleSaveStockTake, selectedSession, currentUserId, onSnackbar, t]);
  410. const uniqueWarehouses = Array.from(
  411. new Set(
  412. inventoryLotDetails
  413. .map((detail) => detail.warehouse)
  414. .filter((warehouse) => warehouse && warehouse.trim() !== "")
  415. )
  416. ).join(", ");
  417. return (
  418. <Box>
  419. <Button
  420. onClick={onBack}
  421. sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}
  422. >
  423. {t("Back to List")}
  424. </Button>
  425. <Typography variant="h6" sx={{ mb: 2 }}>
  426. {t("Stock Take Section")}: {selectedSession.stockTakeSession}
  427. {uniqueWarehouses && (
  428. <> {t("Warehouse")}: {uniqueWarehouses}</>
  429. )}
  430. </Typography>
  431. {/*
  432. <Button
  433. variant="contained"
  434. onClick={handleSubmitAllInputted}
  435. disabled={batchSaving || saving}
  436. >
  437. {t("Submit All Inputted")}
  438. </Button>
  439. */}
  440. {/* 如果需要显示快捷键输入,可以把这块注释打开 */}
  441. {/*
  442. {shortcutInput && (
  443. <Box
  444. sx={{
  445. mb: 2,
  446. p: 1.5,
  447. bgcolor: "info.light",
  448. borderRadius: 1,
  449. border: "1px solid",
  450. borderColor: "info.main",
  451. }}
  452. >
  453. <Typography variant="body2" color="info.dark" fontWeight={500}>
  454. {t("Shortcut Input")}:{" "}
  455. <strong style={{ fontFamily: "monospace", fontSize: "1.1em" }}>
  456. {shortcutInput}
  457. </strong>
  458. </Typography>
  459. </Box>
  460. )}
  461. */}
  462. {loadingDetails ? (
  463. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  464. <CircularProgress />
  465. </Box>
  466. ) : (
  467. <>
  468. <TablePagination
  469. component="div"
  470. count={total}
  471. page={page}
  472. onPageChange={handleChangePage}
  473. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  474. onRowsPerPageChange={handleChangeRowsPerPage}
  475. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  476. labelRowsPerPage={t("Rows per page")}
  477. />
  478. <TableContainer component={Paper}>
  479. <Table>
  480. <TableHead>
  481. <TableRow>
  482. <TableCell>{t("Warehouse Location")}</TableCell>
  483. <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
  484. <TableCell>{t("UOM")}</TableCell>
  485. <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
  486. <TableCell>{t("Action")}</TableCell>
  487. <TableCell>{t("Remark")}</TableCell>
  488. <TableCell>{t("Record Status")}</TableCell>
  489. </TableRow>
  490. </TableHead>
  491. <TableBody>
  492. {inventoryLotDetails.length === 0 ? (
  493. <TableRow>
  494. <TableCell colSpan={7} align="center">
  495. <Typography variant="body2" color="text.secondary">
  496. {t("No data")}
  497. </Typography>
  498. </TableCell>
  499. </TableRow>
  500. ) : (
  501. inventoryLotDetails.map((detail) => {
  502. const submitDisabled = isSubmitDisabled(detail);
  503. const isFirstSubmit =
  504. !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  505. const isSecondSubmit =
  506. detail.stockTakeRecordId &&
  507. detail.firstStockTakeQty &&
  508. !detail.secondStockTakeQty;
  509. return (
  510. <TableRow key={detail.id}>
  511. <TableCell>
  512. {detail.warehouseArea || "-"}
  513. {detail.warehouseSlot || "-"}
  514. </TableCell>
  515. <TableCell
  516. sx={{
  517. maxWidth: 150,
  518. wordBreak: "break-word",
  519. whiteSpace: "normal",
  520. lineHeight: 1.5,
  521. }}
  522. >
  523. <Stack spacing={0.5}>
  524. <Box>
  525. {detail.itemCode || "-"} {detail.itemName || "-"}
  526. </Box>
  527. <Box>{detail.lotNo || "-"}</Box>
  528. <Box>
  529. {detail.expiryDate
  530. ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
  531. : "-"}
  532. </Box>
  533. </Stack>
  534. </TableCell>
  535. <TableCell>{detail.uom || "-"}</TableCell>
  536. {/* Qty + Bad Qty 合并显示/输入 */}
  537. <TableCell sx={{ minWidth: 300 }}>
  538. <Stack spacing={1}>
  539. {/* First */}
  540. {!submitDisabled && isFirstSubmit ? (
  541. <Stack direction="row" spacing={1} alignItems="center">
  542. <Typography variant="body2">{t("First")}:</Typography>
  543. <TextField
  544. size="small"
  545. type="number"
  546. value={recordInputs[detail.id]?.firstQty || ""}
  547. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  548. onKeyDown={blockNonIntegerKeys}
  549. onChange={(e) => {
  550. const clean = sanitizeIntegerInput(e.target.value);
  551. const val = clean;
  552. if (val.includes("-")) return;
  553. setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } }));
  554. }}
  555. sx={{
  556. width: 130,
  557. minWidth: 130,
  558. "& .MuiInputBase-input": {
  559. height: "1.4375em",
  560. padding: "4px 8px",
  561. },
  562. "& .MuiInputBase-input::placeholder": {
  563. color: "grey.400", // MUI light grey
  564. opacity: 1,
  565. },
  566. }}
  567. placeholder={t("Stock Take Qty")}
  568. />
  569. <TextField
  570. size="small"
  571. type="number"
  572. value={recordInputs[detail.id]?.firstBadQty || ""}
  573. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  574. onKeyDown={blockNonIntegerKeys}
  575. onChange={(e) => {
  576. const clean = sanitizeIntegerInput(e.target.value);
  577. const val = clean;
  578. if (val.includes("-")) return;
  579. setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } }));
  580. }}
  581. sx={{
  582. width: 130,
  583. minWidth: 130,
  584. "& .MuiInputBase-input": {
  585. height: "1.4375em",
  586. padding: "4px 8px",
  587. },
  588. "& .MuiInputBase-input::placeholder": {
  589. color: "grey.400", // MUI light grey
  590. opacity: 1,
  591. },
  592. }}
  593. placeholder={t("Bad Qty")}
  594. />
  595. <Typography variant="body2">
  596. =
  597. {formatNumber(
  598. parseFloat(recordInputs[detail.id]?.firstQty || "0") -
  599. parseFloat(recordInputs[detail.id]?.firstBadQty || "0")
  600. )}
  601. </Typography>
  602. </Stack>
  603. ) : detail.firstStockTakeQty != null ? (
  604. <Typography variant="body2">
  605. {t("First")}:{" "}
  606. {formatNumber(
  607. (detail.firstStockTakeQty ?? 0) +
  608. (detail.firstBadQty ?? 0)
  609. )}{" "}
  610. (
  611. {formatNumber(
  612. detail.firstBadQty ?? 0
  613. )}
  614. ) ={" "}
  615. {formatNumber(detail.firstStockTakeQty ?? 0)}
  616. </Typography>
  617. ) : null}
  618. {/* Second */}
  619. {!submitDisabled && isSecondSubmit ? (
  620. <Stack direction="row" spacing={1} alignItems="center">
  621. <Typography variant="body2">{t("Second")}:</Typography>
  622. <TextField
  623. size="small"
  624. type="number"
  625. value={recordInputs[detail.id]?.secondQty || ""}
  626. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  627. onKeyDown={blockNonIntegerKeys}
  628. onChange={(e) => {
  629. const clean = sanitizeIntegerInput(e.target.value);
  630. const val = clean;
  631. if (val.includes("-")) return;
  632. setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } }));
  633. }}
  634. sx={{
  635. width: 130,
  636. minWidth: 130,
  637. "& .MuiInputBase-input": {
  638. height: "1.4375em",
  639. padding: "4px 8px",
  640. },
  641. }}
  642. placeholder={t("Stock Take Qty")}
  643. />
  644. <TextField
  645. size="small"
  646. type="number"
  647. value={recordInputs[detail.id]?.secondBadQty || ""}
  648. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  649. onKeyDown={blockNonIntegerKeys}
  650. onChange={(e) => {
  651. const clean = sanitizeIntegerInput(e.target.value);
  652. const val = clean;
  653. if (val.includes("-")) return;
  654. setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } }));
  655. }}
  656. sx={{
  657. width: 130,
  658. minWidth: 130,
  659. "& .MuiInputBase-input": {
  660. height: "1.4375em",
  661. padding: "4px 8px",
  662. },
  663. }}
  664. placeholder={t("Bad Qty")}
  665. />
  666. <Typography variant="body2">
  667. =
  668. {formatNumber(
  669. parseFloat(recordInputs[detail.id]?.secondQty || "0") -
  670. parseFloat(recordInputs[detail.id]?.secondBadQty || "0")
  671. )}
  672. </Typography>
  673. </Stack>
  674. ) : detail.secondStockTakeQty != null ? (
  675. <Typography variant="body2">
  676. {t("Second")}:{" "}
  677. {formatNumber(
  678. (detail.secondStockTakeQty ?? 0) +
  679. (detail.secondBadQty ?? 0)
  680. )}{" "}
  681. (
  682. {formatNumber(
  683. detail.secondBadQty ?? 0
  684. )}
  685. ) ={" "}
  686. {formatNumber(detail.secondStockTakeQty ?? 0)}
  687. </Typography>
  688. ) : null}
  689. {!detail.firstStockTakeQty &&
  690. !detail.secondStockTakeQty &&
  691. !submitDisabled && (
  692. <Typography
  693. variant="body2"
  694. color="text.secondary"
  695. >
  696. -
  697. </Typography>
  698. )}
  699. </Stack>
  700. </TableCell>
  701. <TableCell>
  702. <Stack direction="row" spacing={1}>
  703. <Button
  704. size="small"
  705. variant="contained"
  706. onClick={() => handleSaveStockTake(detail)}
  707. disabled={saving || submitDisabled}
  708. >
  709. {t("Save")}
  710. </Button>
  711. </Stack>
  712. </TableCell>
  713. {/* Remark */}
  714. <TableCell sx={{ width: 180 }}>
  715. {!submitDisabled && isSecondSubmit ? (
  716. <>
  717. <Typography variant="body2">{t("Remark")}</Typography>
  718. <TextField
  719. size="small"
  720. value={recordInputs[detail.id]?.remark || ""}
  721. // onKeyDown={blockNonIntegerKeys}
  722. //inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
  723. onChange={(e) => {
  724. // const clean = sanitizeIntegerInput(e.target.value);
  725. setRecordInputs(prev => ({
  726. ...prev,
  727. [detail.id]: {
  728. ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }),
  729. remark: e.target.value
  730. }
  731. }));
  732. }}
  733. sx={{ width: 150 }}
  734. />
  735. </>
  736. ) : (
  737. <Typography variant="body2">
  738. {detail.remarks || "-"}
  739. </Typography>
  740. )}
  741. </TableCell>
  742. <TableCell>
  743. {detail.stockTakeRecordStatus === "completed" ? (
  744. <Chip
  745. size="small"
  746. label={t(detail.stockTakeRecordStatus)}
  747. color="success"
  748. />
  749. ) : detail.stockTakeRecordStatus === "pass" ? (
  750. <Chip
  751. size="small"
  752. label={t(detail.stockTakeRecordStatus)}
  753. color="default"
  754. />
  755. ) : detail.stockTakeRecordStatus === "notMatch" ? (
  756. <Chip
  757. size="small"
  758. label={t(detail.stockTakeRecordStatus)}
  759. color="warning"
  760. />
  761. ) : (
  762. <Chip
  763. size="small"
  764. label={t(detail.stockTakeRecordStatus || "")}
  765. color="default"
  766. />
  767. )}
  768. </TableCell>
  769. </TableRow>
  770. );
  771. })
  772. )}
  773. </TableBody>
  774. </Table>
  775. </TableContainer>
  776. <TablePagination
  777. component="div"
  778. count={total}
  779. page={page}
  780. onPageChange={handleChangePage}
  781. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  782. onRowsPerPageChange={handleChangeRowsPerPage}
  783. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  784. labelRowsPerPage={t("Rows per page")}
  785. />
  786. </>
  787. )}
  788. </Box>
  789. );
  790. };
  791. export default PickerStockTake;