FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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