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.

PickerStockTake.tsx 33 KiB

3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 päivä sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
3 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
2 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
3 kuukautta sitten
1 kuukausi sitten
2 viikkoa sitten
2 viikkoa sitten
2 viikkoa sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
3 kuukautta sitten
2 kuukautta sitten
1 kuukausi sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
1 kuukausi sitten
2 kuukautta sitten
3 kuukautta sitten
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;