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

PickerReStockTake.tsx 26 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1週間前
1週間前
1週間前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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. } from "@mui/material";
  19. import { useState, useCallback, useEffect, useRef } from "react";
  20. import { useTranslation } from "react-i18next";
  21. import {
  22. AllPickedStockTakeListReponse,
  23. InventoryLotDetailResponse,
  24. saveStockTakeRecord,
  25. SaveStockTakeRecordRequest,
  26. BatchSaveStockTakeRecordRequest,
  27. batchSaveStockTakeRecords,
  28. getInventoryLotDetailsBySectionNotMatch
  29. } from "@/app/api/stockTake/actions";
  30. import { useSession } from "next-auth/react";
  31. import { SessionWithTokens } from "@/config/authConfig";
  32. import dayjs from "dayjs";
  33. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  34. interface PickerReStockTakeProps {
  35. selectedSession: AllPickedStockTakeListReponse;
  36. onBack: () => void;
  37. onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
  38. }
  39. const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
  40. selectedSession,
  41. onBack,
  42. onSnackbar,
  43. }) => {
  44. const { t } = useTranslation(["inventory", "common"]);
  45. const { data: session } = useSession() as { data: SessionWithTokens | null };
  46. const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
  47. const [loadingDetails, setLoadingDetails] = useState(false);
  48. const [recordInputs, setRecordInputs] = useState<Record<number, {
  49. firstQty: string;
  50. secondQty: string;
  51. firstBadQty: string;
  52. secondBadQty: string;
  53. remark: string;
  54. }>>({});
  55. const [saving, setSaving] = useState(false);
  56. const [batchSaving, setBatchSaving] = useState(false);
  57. const [shortcutInput, setShortcutInput] = useState<string>("");
  58. const [page, setPage] = useState(0);
  59. const [pageSize, setPageSize] = useState<number | string>("all");
  60. const [total, setTotal] = useState(0);
  61. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  62. const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
  63. const handleChangePage = useCallback((event: unknown, newPage: number) => {
  64. setPage(newPage);
  65. }, []);
  66. const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => {
  67. // 禁止小数点、逗号、科学计数、正负号
  68. if ([".", ",", "e", "E", "+", "-"].includes(e.key)) {
  69. e.preventDefault();
  70. }
  71. };
  72. const sanitizeIntegerInput = (value: string) => {
  73. // 只保留数字
  74. return value.replace(/[^\d]/g, "");
  75. };
  76. const isIntegerString = (value: string) => /^\d+$/.test(value);
  77. const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  78. const newSize = parseInt(event.target.value, 10);
  79. if (newSize === -1) {
  80. setPageSize("all");
  81. } else if (!isNaN(newSize)) {
  82. setPageSize(newSize);
  83. }
  84. setPage(0);
  85. }, []);
  86. const loadDetails = useCallback(async (pageNum: number, size: number | string) => {
  87. setLoadingDetails(true);
  88. try {
  89. let actualSize: number;
  90. if (size === "all") {
  91. if (selectedSession.totalInventoryLotNumber > 0) {
  92. actualSize = selectedSession.totalInventoryLotNumber;
  93. } else if (total > 0) {
  94. actualSize = total;
  95. } else {
  96. actualSize = 10000;
  97. }
  98. } else {
  99. actualSize = typeof size === 'string' ? parseInt(size, 10) : size;
  100. }
  101. const response = await getInventoryLotDetailsBySectionNotMatch(
  102. selectedSession.stockTakeSession,
  103. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
  104. pageNum,
  105. actualSize
  106. );
  107. setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
  108. setTotal(response.total || 0);
  109. } catch (e) {
  110. console.error(e);
  111. setInventoryLotDetails([]);
  112. setTotal(0);
  113. } finally {
  114. setLoadingDetails(false);
  115. }
  116. }, [selectedSession, total]);
  117. useEffect(() => {
  118. const inputs: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {};
  119. inventoryLotDetails.forEach((detail) => {
  120. const firstTotal = detail.firstStockTakeQty != null
  121. ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString()
  122. : "";
  123. const secondTotal = detail.secondStockTakeQty != null
  124. ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString()
  125. : "";
  126. inputs[detail.id] = {
  127. firstQty: firstTotal,
  128. secondQty: secondTotal,
  129. firstBadQty: detail.firstBadQty?.toString() || "",
  130. secondBadQty: detail.secondBadQty?.toString() || "",
  131. remark: detail.remarks || "",
  132. };
  133. });
  134. setRecordInputs(inputs);
  135. }, [inventoryLotDetails]);
  136. useEffect(() => {
  137. loadDetails(page, pageSize);
  138. }, [page, pageSize, loadDetails]);
  139. const formatNumber = (num: number | null | undefined): string => {
  140. if (num == null || Number.isNaN(num)) return "0";
  141. return num.toLocaleString("en-US", {
  142. minimumFractionDigits: 0,
  143. maximumFractionDigits: 0,
  144. });
  145. };
  146. const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
  147. if (!selectedSession || !currentUserId) {
  148. return;
  149. }
  150. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  151. const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  152. // 用戶輸入為 total 和 bad,需計算 available = total - bad(與 PickerStockTake 一致)
  153. const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty;
  154. const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty;
  155. if (!totalQtyStr) {
  156. onSnackbar(
  157. isFirstSubmit
  158. ? t("Please enter QTY")
  159. : t("Please enter Second QTY"),
  160. "error"
  161. );
  162. return;
  163. }
  164. const totalQty = parseFloat(totalQtyStr);
  165. const badQty = parseFloat(badQtyStr || "0") || 0;
  166. if (Number.isNaN(totalQty)) {
  167. onSnackbar(t("Invalid QTY"), "error");
  168. return;
  169. }
  170. const availableQty = totalQty - badQty;
  171. if (availableQty < 0) {
  172. onSnackbar(t("Available QTY cannot be negative"), "error");
  173. return;
  174. }
  175. setSaving(true);
  176. try {
  177. const request: SaveStockTakeRecordRequest = {
  178. stockTakeRecordId: detail.stockTakeRecordId || null,
  179. inventoryLotLineId: detail.id,
  180. qty: availableQty,
  181. badQty: badQty,
  182. remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null,
  183. };
  184. const result = await saveStockTakeRecord(
  185. request,
  186. selectedSession.stockTakeId,
  187. currentUserId
  188. );
  189. onSnackbar(t("Stock take record saved successfully"), "success");
  190. const savedId = result?.id ?? detail.stockTakeRecordId;
  191. setInventoryLotDetails((prev) =>
  192. prev.map((d) =>
  193. d.id === detail.id
  194. ? {
  195. ...d,
  196. stockTakeRecordId: savedId ?? d.stockTakeRecordId,
  197. firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty,
  198. firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null,
  199. secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty,
  200. secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null,
  201. remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks,
  202. stockTakeRecordStatus: "pass",
  203. }
  204. : d
  205. )
  206. );
  207. } catch (e: any) {
  208. console.error("Save stock take record error:", e);
  209. let errorMessage = t("Failed to save stock take record");
  210. if (e?.message) {
  211. errorMessage = e.message;
  212. } else if (e?.response) {
  213. try {
  214. const errorData = await e.response.json();
  215. errorMessage = errorData.message || errorData.error || errorMessage;
  216. } catch {
  217. // ignore
  218. }
  219. }
  220. onSnackbar(errorMessage, "error");
  221. } finally {
  222. setSaving(false);
  223. }
  224. }, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);
  225. const handleBatchSubmitAll = useCallback(async () => {
  226. if (!selectedSession || !currentUserId) {
  227. return;
  228. }
  229. setBatchSaving(true);
  230. try {
  231. const request: BatchSaveStockTakeRecordRequest = {
  232. stockTakeId: selectedSession.stockTakeId,
  233. stockTakeSection: selectedSession.stockTakeSession,
  234. stockTakerId: currentUserId,
  235. };
  236. const result = await batchSaveStockTakeRecords(request);
  237. onSnackbar(
  238. t("Batch save completed: {{success}} success, {{errors}} errors", {
  239. success: result.successCount,
  240. errors: result.errorCount,
  241. }),
  242. result.errorCount > 0 ? "warning" : "success"
  243. );
  244. await loadDetails(page, pageSize);
  245. } catch (e: any) {
  246. console.error("handleBatchSubmitAll: Error:", e);
  247. let errorMessage = t("Failed to batch save stock take records");
  248. if (e?.message) {
  249. errorMessage = e.message;
  250. } else if (e?.response) {
  251. try {
  252. const errorData = await e.response.json();
  253. errorMessage = errorData.message || errorData.error || errorMessage;
  254. } catch {
  255. // ignore
  256. }
  257. }
  258. onSnackbar(errorMessage, "error");
  259. } finally {
  260. setBatchSaving(false);
  261. }
  262. }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);
  263. useEffect(() => {
  264. handleBatchSubmitAllRef.current = handleBatchSubmitAll;
  265. }, [handleBatchSubmitAll]);
  266. useEffect(() => {
  267. const handleKeyPress = (e: KeyboardEvent) => {
  268. const target = e.target as HTMLElement;
  269. if (target && (
  270. target.tagName === 'INPUT' ||
  271. target.tagName === 'TEXTAREA' ||
  272. target.isContentEditable
  273. )) {
  274. return;
  275. }
  276. if (e.ctrlKey || e.metaKey || e.altKey) {
  277. return;
  278. }
  279. if (e.key.length === 1) {
  280. setShortcutInput(prev => {
  281. const newInput = prev + e.key;
  282. if (newInput === '{2fitestall}') {
  283. setTimeout(() => {
  284. if (handleBatchSubmitAllRef.current) {
  285. handleBatchSubmitAllRef.current().catch(err => {
  286. console.error('Error in handleBatchSubmitAll:', err);
  287. });
  288. }
  289. }, 0);
  290. return "";
  291. }
  292. if (newInput.length > 15) return "";
  293. if (newInput.length > 0 && !newInput.startsWith('{')) return "";
  294. if (newInput.length > 5 && !newInput.startsWith('{2fi')) return "";
  295. return newInput;
  296. });
  297. } else if (e.key === 'Backspace') {
  298. setShortcutInput(prev => prev.slice(0, -1));
  299. } else if (e.key === 'Escape') {
  300. setShortcutInput("");
  301. }
  302. };
  303. window.addEventListener('keydown', handleKeyPress);
  304. return () => {
  305. window.removeEventListener('keydown', handleKeyPress);
  306. };
  307. }, []);
  308. const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
  309. if (selectedSession?.status?.toLowerCase() === "completed") {
  310. return true;
  311. }
  312. const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
  313. if (recordStatus === "pass" || recordStatus === "completed") {
  314. return true;
  315. }
  316. return false;
  317. }, [selectedSession?.status]);
  318. const uniqueWarehouses = Array.from(
  319. new Set(
  320. inventoryLotDetails
  321. .map(detail => detail.warehouse)
  322. .filter(warehouse => warehouse && warehouse.trim() !== "")
  323. )
  324. ).join(", ");
  325. const defaultInputs = { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" };
  326. return (
  327. <Box>
  328. <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
  329. {t("Back to List")}
  330. </Button>
  331. <Typography variant="h6" sx={{ mb: 2 }}>
  332. {t("Stock Take Section")}: {selectedSession.stockTakeSession}
  333. {uniqueWarehouses && (
  334. <> {t("Warehouse")}: {uniqueWarehouses}</>
  335. )}
  336. </Typography>
  337. {loadingDetails ? (
  338. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  339. <CircularProgress />
  340. </Box>
  341. ) : (
  342. <>
  343. <TablePagination
  344. component="div"
  345. count={total}
  346. page={page}
  347. onPageChange={handleChangePage}
  348. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  349. onRowsPerPageChange={handleChangeRowsPerPage}
  350. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  351. labelRowsPerPage={t("Rows per page")}
  352. />
  353. <TableContainer component={Paper}>
  354. <Table>
  355. <TableHead>
  356. <TableRow>
  357. <TableCell>{t("Warehouse Location")}</TableCell>
  358. <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
  359. <TableCell>{t("UOM")}</TableCell>
  360. <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
  361. <TableCell>{t("Action")}</TableCell>
  362. <TableCell>{t("Remark")}</TableCell>
  363. <TableCell>{t("Record Status")}</TableCell>
  364. </TableRow>
  365. </TableHead>
  366. <TableBody>
  367. {inventoryLotDetails.length === 0 ? (
  368. <TableRow>
  369. <TableCell colSpan={7} align="center">
  370. <Typography variant="body2" color="text.secondary">
  371. {t("No data")}
  372. </Typography>
  373. </TableCell>
  374. </TableRow>
  375. ) : (
  376. inventoryLotDetails.map((detail) => {
  377. const submitDisabled = isSubmitDisabled(detail);
  378. const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
  379. const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
  380. const inputs = recordInputs[detail.id] ?? defaultInputs;
  381. return (
  382. <TableRow key={detail.id}>
  383. <TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
  384. <TableCell sx={{
  385. maxWidth: 150,
  386. wordBreak: 'break-word',
  387. whiteSpace: 'normal',
  388. lineHeight: 1.5
  389. }}>
  390. <Stack spacing={0.5}>
  391. <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
  392. <Box>{detail.lotNo || "-"}</Box>
  393. <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
  394. </Stack>
  395. </TableCell>
  396. <TableCell>{detail.uom || "-"}</TableCell>
  397. <TableCell sx={{ minWidth: 300 }}>
  398. <Stack spacing={1}>
  399. {/* First */}
  400. {!submitDisabled && isFirstSubmit ? (
  401. <Stack direction="row" spacing={1} alignItems="center">
  402. <Typography variant="body2">{t("First")}:</Typography>
  403. <TextField
  404. size="small"
  405. type="number"
  406. value={inputs.firstQty}
  407. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  408. onKeyDown={blockNonIntegerKeys}
  409. onChange={(e) => {
  410. const clean = sanitizeIntegerInput(e.target.value);
  411. const val = clean;
  412. if (val.includes("-")) return;
  413. setRecordInputs(prev => ({
  414. ...prev,
  415. [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstQty: val }
  416. }));
  417. }}
  418. sx={{
  419. width: 130,
  420. minWidth: 130,
  421. "& .MuiInputBase-input": {
  422. height: "1.4375em",
  423. padding: "4px 8px",
  424. },
  425. }}
  426. placeholder={t("Stock Take Qty")}
  427. />
  428. <TextField
  429. size="small"
  430. type="number"
  431. value={inputs.firstBadQty}
  432. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  433. onKeyDown={blockNonIntegerKeys}
  434. onChange={(e) => {
  435. const clean = sanitizeIntegerInput(e.target.value);
  436. const val = clean;
  437. if (val.includes("-")) return;
  438. setRecordInputs(prev => ({
  439. ...prev,
  440. [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstBadQty: val }
  441. }));
  442. }}
  443. sx={{
  444. width: 130,
  445. minWidth: 130,
  446. "& .MuiInputBase-input": {
  447. height: "1.4375em",
  448. padding: "4px 8px",
  449. },
  450. }}
  451. placeholder={t("Bad Qty")}
  452. />
  453. <Typography variant="body2">
  454. = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))}
  455. </Typography>
  456. </Stack>
  457. ) : detail.firstStockTakeQty != null ? (
  458. <Typography variant="body2">
  459. {t("First")}:{" "}
  460. {formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "}
  461. ({formatNumber(detail.firstBadQty ?? 0)}) ={" "}
  462. {formatNumber(detail.firstStockTakeQty ?? 0)}
  463. </Typography>
  464. ) : null}
  465. {/* Second */}
  466. {!submitDisabled && isSecondSubmit ? (
  467. <Stack direction="row" spacing={1} alignItems="center">
  468. <Typography variant="body2">{t("Second")}:</Typography>
  469. <TextField
  470. size="small"
  471. type="number"
  472. value={inputs.secondQty}
  473. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  474. onKeyDown={blockNonIntegerKeys}
  475. onChange={(e) => {
  476. const clean = sanitizeIntegerInput(e.target.value);
  477. const val = clean;
  478. if (val.includes("-")) return;
  479. setRecordInputs(prev => ({
  480. ...prev,
  481. [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: clean }
  482. }));
  483. }}
  484. sx={{
  485. width: 130,
  486. minWidth: 130,
  487. "& .MuiInputBase-input": {
  488. height: "1.4375em",
  489. padding: "4px 8px",
  490. },
  491. }}
  492. placeholder={t("Stock Take Qty")}
  493. />
  494. <TextField
  495. size="small"
  496. type="number"
  497. value={inputs.secondBadQty}
  498. inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
  499. onKeyDown={blockNonIntegerKeys}
  500. onChange={(e) => {
  501. const clean = sanitizeIntegerInput(e.target.value);
  502. const val = clean;
  503. if (val.includes("-")) return;
  504. setRecordInputs(prev => ({
  505. ...prev,
  506. [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: clean }
  507. }));
  508. }}
  509. sx={{
  510. width: 130,
  511. minWidth: 130,
  512. "& .MuiInputBase-input": {
  513. height: "1.4375em",
  514. padding: "4px 8px",
  515. },
  516. }}
  517. placeholder={t("Bad Qty")}
  518. />
  519. <Typography variant="body2">
  520. = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))}
  521. </Typography>
  522. </Stack>
  523. ) : detail.secondStockTakeQty != null ? (
  524. <Typography variant="body2">
  525. {t("Second")}:{" "}
  526. {formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "}
  527. ({formatNumber(detail.secondBadQty ?? 0)}) ={" "}
  528. {formatNumber(detail.secondStockTakeQty ?? 0)}
  529. </Typography>
  530. ) : null}
  531. {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && (
  532. <Typography variant="body2" color="text.secondary">
  533. -
  534. </Typography>
  535. )}
  536. </Stack>
  537. </TableCell>
  538. <TableCell>
  539. <Stack direction="row" spacing={1}>
  540. <Button
  541. size="small"
  542. variant="contained"
  543. onClick={() => handleSaveStockTake(detail)}
  544. disabled={saving || submitDisabled }
  545. >
  546. {t("Save")}
  547. </Button>
  548. </Stack>
  549. </TableCell>
  550. <TableCell sx={{ width: 180 }}>
  551. {!submitDisabled && isSecondSubmit ? (
  552. <>
  553. <Typography variant="body2">{t("Remark")}</Typography>
  554. <TextField
  555. size="small"
  556. value={inputs.remark}
  557. // onKeyDown={blockNonIntegerKeys}
  558. //inputProps={{ inputMode: "text", pattern: "[0-9]*" }}
  559. onChange={(e) => {
  560. // const clean = sanitizeIntegerInput(e.target.value);
  561. setRecordInputs(prev => ({
  562. ...prev,
  563. [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value }
  564. }));
  565. }}
  566. sx={{ width: 150 }}
  567. />
  568. </>
  569. ) : (
  570. <Typography variant="body2">
  571. {detail.remarks || "-"}
  572. </Typography>
  573. )}
  574. </TableCell>
  575. <TableCell>
  576. {detail.stockTakeRecordStatus === "completed" ? (
  577. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
  578. ) : detail.stockTakeRecordStatus === "pass" ? (
  579. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" />
  580. ) : detail.stockTakeRecordStatus === "notMatch" ? (
  581. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
  582. ) : (
  583. <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
  584. )}
  585. </TableCell>
  586. </TableRow>
  587. );
  588. })
  589. )}
  590. </TableBody>
  591. </Table>
  592. </TableContainer>
  593. <TablePagination
  594. component="div"
  595. count={total}
  596. page={page}
  597. onPageChange={handleChangePage}
  598. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  599. onRowsPerPageChange={handleChangeRowsPerPage}
  600. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  601. labelRowsPerPage={t("Rows per page")}
  602. />
  603. </>
  604. )}
  605. </Box>
  606. );
  607. };
  608. export default PickerReStockTake;