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.
 
 

772 lines
29 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. Checkbox,
  17. TextField,
  18. FormControlLabel,
  19. Radio,
  20. TablePagination,
  21. ToggleButton
  22. } from "@mui/material";
  23. import { useState, useCallback, useEffect, useRef, useMemo } from "react";
  24. import { useTranslation } from "react-i18next";
  25. import {
  26. AllPickedStockTakeListReponse,
  27. getInventoryLotDetailsBySection,
  28. InventoryLotDetailResponse,
  29. saveApproverStockTakeRecord,
  30. SaveApproverStockTakeRecordRequest,
  31. BatchSaveApproverStockTakeRecordRequest,
  32. batchSaveApproverStockTakeRecords,
  33. updateStockTakeRecordStatusToNotMatch,
  34. } from "@/app/api/stockTake/actions";
  35. import { stockTakeQtyEndAdornment } from "./stockTakeQtyAdornment";
  36. import StockTakeQtyGapHint from "./StockTakeQtyGapHint";
  37. import { stockTakeHiddenOnHand, stockTakeQtyGapWarnText } from "./stockTakeQtyGapWarning";
  38. import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent";
  39. import { useSession } from "next-auth/react";
  40. import { SessionWithTokens } from "@/config/authConfig";
  41. import dayjs from "dayjs";
  42. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  43. interface ApproverStockTakeProps {
  44. selectedSession: AllPickedStockTakeListReponse;
  45. onBack: () => void;
  46. onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
  47. }
  48. type QtySelectionType = "first" | "second" | "approver";
  49. const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
  50. selectedSession,
  51. onBack,
  52. onSnackbar,
  53. }) => {
  54. const { t } = useTranslation(["stockTake", "common"]);
  55. const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
  56. const { data: session } = useSession() as { data: SessionWithTokens | null };
  57. const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
  58. const [loadingDetails, setLoadingDetails] = useState(false);
  59. const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(true);
  60. const [variancePercentTolerance, setVariancePercentTolerance] = useState<string>("5");
  61. // 每个记录的选择状态,key 为 detail.id
  62. const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({});
  63. const [approverQty, setApproverQty] = useState<Record<number, string>>({});
  64. const [gapCheckOpen, setGapCheckOpen] = useState<Record<string, boolean>>({});
  65. const [approverBadQty, setApproverBadQty] = useState<Record<number, string>>({});
  66. const [saving, setSaving] = useState(false);
  67. const [batchSaving, setBatchSaving] = useState(false);
  68. const [updatingStatus, setUpdatingStatus] = useState(false);
  69. const [page, setPage] = useState(0);
  70. const [pageSize, setPageSize] = useState<number | string>("all");
  71. const [total, setTotal] = useState(0);
  72. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  73. const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
  74. const isWithinVarianceTolerance = useCallback((
  75. difference: number,
  76. bookQty: number,
  77. percentStr: string
  78. ): boolean => {
  79. const percent = parseFloat(percentStr || "0");
  80. if (isNaN(percent) || percent < 0) return true; // 无效输入时视为全部通过
  81. if (bookQty === 0) return difference === 0;
  82. const threshold = Math.abs(bookQty) * (percent / 100);
  83. return Math.abs(difference) <= threshold;
  84. }, []);
  85. const handleChangePage = useCallback((event: unknown, newPage: number) => {
  86. setPage(newPage);
  87. }, []);
  88. const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  89. const newSize = parseInt(event.target.value, 10);
  90. if (newSize === -1) {
  91. setPageSize("all");
  92. } else if (!isNaN(newSize)) {
  93. setPageSize(newSize);
  94. }
  95. setPage(0);
  96. }, []);
  97. const loadDetails = useCallback(async (pageNum: number, size: number | string) => {
  98. setLoadingDetails(true);
  99. try {
  100. let actualSize: number;
  101. if (size === "all") {
  102. if (selectedSession.totalInventoryLotNumber > 0) {
  103. actualSize = selectedSession.totalInventoryLotNumber;
  104. } else if (total > 0) {
  105. actualSize = total;
  106. } else {
  107. actualSize = 10000;
  108. }
  109. } else {
  110. actualSize = typeof size === 'string' ? parseInt(size, 10) : size;
  111. }
  112. const response = await getInventoryLotDetailsBySection(
  113. selectedSession.stockTakeSession,
  114. selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
  115. pageNum,
  116. actualSize,
  117. selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0
  118. ? selectedSession.stockTakeRoundId
  119. : null
  120. );
  121. setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
  122. setTotal(response.total || 0);
  123. } catch (e) {
  124. console.error(e);
  125. setInventoryLotDetails([]);
  126. setTotal(0);
  127. } finally {
  128. setLoadingDetails(false);
  129. }
  130. }, [selectedSession, total]);
  131. useEffect(() => {
  132. loadDetails(page, pageSize);
  133. }, [page, pageSize, loadDetails]);
  134. const calculateDifference = useCallback((detail: InventoryLotDetailResponse, selection: QtySelectionType): number => {
  135. let selectedQty = 0;
  136. if (selection === "first") {
  137. selectedQty = detail.firstStockTakeQty || 0;
  138. } else if (selection === "second") {
  139. selectedQty = detail.secondStockTakeQty || 0;
  140. } else if (selection === "approver") {
  141. selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0;
  142. }
  143. const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
  144. return selectedQty - bookQty;
  145. }, [approverQty, approverBadQty]);
  146. // 3. 修改默认选择逻辑(在 loadDetails 的 useEffect 中,或创建一个新的 useEffect)
  147. useEffect(() => {
  148. // 初始化默认选择:如果 second 存在则选择 second,否则选择 first
  149. const newSelections: Record<number, QtySelectionType> = {};
  150. inventoryLotDetails.forEach(detail => {
  151. if (!qtySelection[detail.id]) {
  152. // 如果 second 不为 null 且大于 0,默认选择 second,否则选择 first
  153. if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) {
  154. newSelections[detail.id] = "second";
  155. } else {
  156. newSelections[detail.id] = "first";
  157. }
  158. }
  159. });
  160. if (Object.keys(newSelections).length > 0) {
  161. setQtySelection(prev => ({ ...prev, ...newSelections }));
  162. }
  163. }, [inventoryLotDetails]);
  164. // 4. 添加过滤逻辑(在渲染表格之前)
  165. const filteredDetails = useMemo(() => {
  166. let result = inventoryLotDetails;
  167. if (showOnlyWithDifference) {
  168. const percent = parseFloat(variancePercentTolerance || "0");
  169. const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent;
  170. result = result.filter(detail => {
  171. // 已完成項目一律顯示
  172. if (detail.finalQty != null || detail.stockTakeRecordStatus === "completed") {
  173. return true;
  174. }
  175. const selection = qtySelection[detail.id] ??
  176. (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first");
  177. const difference = calculateDifference(detail, selection);
  178. const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
  179. if (bookQty === 0) return difference !== 0;
  180. const threshold = Math.abs(bookQty) * (thresholdPercent / 100);
  181. return Math.abs(difference) > threshold;
  182. });
  183. }
  184. return result;
  185. }, [inventoryLotDetails, showOnlyWithDifference, variancePercentTolerance, qtySelection, calculateDifference]);
  186. const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
  187. if (!selectedSession || !currentUserId) {
  188. return;
  189. }
  190. const selection = qtySelection[detail.id] || "first";
  191. let finalQty: number;
  192. let finalBadQty: number;
  193. if (selection === "first") {
  194. if (detail.firstStockTakeQty == null) {
  195. onSnackbar(t("First QTY is not available"), "error");
  196. return;
  197. }
  198. finalQty = detail.firstStockTakeQty;
  199. finalBadQty = detail.firstBadQty || 0;
  200. } else if (selection === "second") {
  201. if (detail.secondStockTakeQty == null) {
  202. onSnackbar(t("Second QTY is not available"), "error");
  203. return;
  204. }
  205. finalQty = detail.secondStockTakeQty;
  206. finalBadQty = detail.secondBadQty || 0;
  207. } else {
  208. // Approver input
  209. const approverQtyValue = approverQty[detail.id];
  210. const approverBadQtyValue = approverBadQty[detail.id];
  211. if (approverQtyValue === undefined || approverQtyValue === null || approverQtyValue === "") {
  212. onSnackbar(t("Please enter Approver QTY"), "error");
  213. return;
  214. }
  215. if (approverBadQtyValue === undefined || approverBadQtyValue === null || approverBadQtyValue === "") {
  216. onSnackbar(t("Please enter Approver Bad QTY"), "error");
  217. return;
  218. }
  219. finalQty = parseFloat(approverQtyValue) || 0;
  220. finalBadQty = parseFloat(approverBadQtyValue) || 0;
  221. }
  222. setSaving(true);
  223. try {
  224. const request: SaveApproverStockTakeRecordRequest = {
  225. stockTakeRecordId: detail.stockTakeRecordId || null,
  226. qty: finalQty,
  227. badQty: finalBadQty,
  228. approverId: currentUserId,
  229. approverQty: selection === "approver" ? finalQty : null,
  230. approverBadQty: selection === "approver" ? finalBadQty : null,
  231. };
  232. await saveApproverStockTakeRecord(
  233. request,
  234. selectedSession.stockTakeId
  235. );
  236. const gapText =
  237. selection === "approver"
  238. ? stockTakeQtyGapWarnText(t, approverQty[detail.id] || "", stockTakeHiddenOnHand(detail), qtyGapWarnPercent)
  239. : null;
  240. if (selection === "approver") {
  241. setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: true }));
  242. }
  243. onSnackbar(
  244. gapText
  245. ? `${t("Approver stock take record saved successfully")} ${gapText}`
  246. : t("Approver stock take record saved successfully"),
  247. gapText ? "warning" : "success",
  248. );
  249. // 計算最終數量(合格數)
  250. const goodQty = finalQty - finalBadQty;
  251. setInventoryLotDetails((prev) =>
  252. prev.map((d) =>
  253. d.id === detail.id
  254. ? {
  255. ...d,
  256. finalQty: goodQty,
  257. approverQty: selection === "approver" ? finalQty : d.approverQty,
  258. approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty,
  259. stockTakeRecordStatus: "completed",
  260. }
  261. : d
  262. )
  263. );
  264. } catch (e: any) {
  265. console.error("Save approver stock take record error:", e);
  266. let errorMessage = t("Failed to save approver stock take record");
  267. if (e?.message) {
  268. errorMessage = e.message;
  269. } else if (e?.response) {
  270. try {
  271. const errorData = await e.response.json();
  272. errorMessage = errorData.message || errorData.error || errorMessage;
  273. } catch {
  274. // ignore
  275. }
  276. }
  277. onSnackbar(errorMessage, "error");
  278. } finally {
  279. setSaving(false);
  280. }
  281. }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar, page, pageSize, loadDetails, qtyGapWarnPercent]);
  282. const handleUpdateStatusToNotMatch = useCallback(async (detail: InventoryLotDetailResponse) => {
  283. if (!detail.stockTakeRecordId) {
  284. onSnackbar(t("Stock take record ID is required"), "error");
  285. return;
  286. }
  287. setUpdatingStatus(true);
  288. try {
  289. await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId);
  290. onSnackbar(t("Stock take record status updated to not match"), "success");
  291. setInventoryLotDetails((prev) =>
  292. prev.map((d) =>
  293. d.id === detail.id ? { ...d, stockTakeRecordStatus: "notMatch" } : d
  294. )
  295. );
  296. } catch (e: any) {
  297. console.error("Update stock take record status error:", e);
  298. let errorMessage = t("Failed to update stock take record status");
  299. if (e?.message) {
  300. errorMessage = e.message;
  301. } else if (e?.response) {
  302. try {
  303. const errorData = await e.response.json();
  304. errorMessage = errorData.message || errorData.error || errorMessage;
  305. } catch {
  306. // ignore
  307. }
  308. }
  309. onSnackbar(errorMessage, "error");
  310. } finally {
  311. setUpdatingStatus(false);
  312. // Reload after status update - the useEffect will handle it with current page/pageSize
  313. // Or explicitly reload:
  314. }
  315. }, [selectedSession, t, onSnackbar, ]);
  316. const handleBatchSubmitAll = useCallback(async () => {
  317. if (!selectedSession || !currentUserId) {
  318. console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId');
  319. return;
  320. }
  321. console.log('handleBatchSubmitAll: Starting batch approver save...');
  322. setBatchSaving(true);
  323. try {
  324. const request: BatchSaveApproverStockTakeRecordRequest = {
  325. stockTakeId: selectedSession.stockTakeId,
  326. stockTakeSection: selectedSession.stockTakeSession,
  327. approverId: currentUserId,
  328. variancePercentTolerance: parseFloat(variancePercentTolerance || "0") || undefined,
  329. };
  330. const result = await batchSaveApproverStockTakeRecords(request);
  331. console.log('handleBatchSubmitAll: Result:', result);
  332. onSnackbar(
  333. t("Batch approver save completed: {{success}} success, {{errors}} errors", {
  334. success: result.successCount,
  335. errors: result.errorCount,
  336. }),
  337. result.errorCount > 0 ? "warning" : "success"
  338. );
  339. await loadDetails(page, pageSize);
  340. } catch (e: any) {
  341. console.error("handleBatchSubmitAll: Error:", e);
  342. let errorMessage = t("Failed to batch save approver stock take records");
  343. if (e?.message) {
  344. errorMessage = e.message;
  345. } else if (e?.response) {
  346. try {
  347. const errorData = await e.response.json();
  348. errorMessage = errorData.message || errorData.error || errorMessage;
  349. } catch {
  350. // ignore
  351. }
  352. }
  353. onSnackbar(errorMessage, "error");
  354. } finally {
  355. setBatchSaving(false);
  356. }
  357. }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);
  358. useEffect(() => {
  359. handleBatchSubmitAllRef.current = handleBatchSubmitAll;
  360. }, [handleBatchSubmitAll]);
  361. const formatNumber = (num: number | null | undefined): string => {
  362. if (num == null) return "0";
  363. return num.toLocaleString('en-US', {
  364. minimumFractionDigits: 0,
  365. maximumFractionDigits: 0
  366. });
  367. };
  368. const uniqueWarehouses = Array.from(
  369. new Set(
  370. inventoryLotDetails
  371. .map(detail => detail.warehouse)
  372. .filter(warehouse => warehouse && warehouse.trim() !== "")
  373. )
  374. ).join(", ");
  375. const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
  376. // 如果已经有 finalQty(已完成审批),不允许再次编辑
  377. if (detail.finalQty != null) {
  378. return true;
  379. }
  380. // 获取当前选择模式
  381. const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0;
  382. const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0;
  383. const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first");
  384. // 如果选择了 "approver" 模式,检查用户是否已经输入了值
  385. if (selection === "approver") {
  386. const approverQtyValue = approverQty[detail.id];
  387. const approverBadQtyValue = approverBadQty[detail.id];
  388. // 如果用户已经输入了值(包括0),允许保存
  389. if (approverQtyValue !== undefined && approverQtyValue !== null && approverQtyValue !== "" &&
  390. approverBadQtyValue !== undefined && approverBadQtyValue !== null && approverBadQtyValue !== "") {
  391. return false; // 允许保存
  392. }
  393. // 如果用户还没有输入值,禁用按钮
  394. return true;
  395. }
  396. // 对于 first 或 second 模式,需要检查是否有有效的数量(允许0)
  397. // 只要 firstStockTakeQty 不为 null,就允许保存(即使为0)
  398. if (detail.firstStockTakeQty == null) {
  399. return true; // 如果 firstStockTakeQty 为 null,禁用
  400. }
  401. return false; // 允许保存
  402. }, [qtySelection, approverQty, approverBadQty]);
  403. return (
  404. <Box>
  405. <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
  406. {t("Back to List")}
  407. </Button>
  408. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
  409. <Typography variant="h6" sx={{ mb: 2 }}>
  410. {t("Stock Take Section")}: {selectedSession.stockTakeSession}
  411. {uniqueWarehouses && (
  412. <> {t("Warehouse")}: {uniqueWarehouses}</>
  413. )}
  414. </Typography>
  415. <Stack direction="row" spacing={2} alignItems="center">
  416. <TextField
  417. size="small"
  418. type="number"
  419. value={variancePercentTolerance}
  420. onChange={(e) => setVariancePercentTolerance(e.target.value)}
  421. label={t("Variance %")}
  422. sx={{ width: 100 }}
  423. inputProps={{ min: 0, max: 100, step: 0.1 }}
  424. />
  425. {/*
  426. <FormControlLabel
  427. control={
  428. <Checkbox
  429. checked={showOnlyWithDifference}
  430. onChange={(e) => setShowOnlyWithDifference(e.target.checked)}
  431. />
  432. }
  433. label={t("Only Variance")}
  434. />
  435. */}
  436. <Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
  437. {t("Batch Save All")}
  438. </Button>
  439. </Stack>
  440. </Stack>
  441. {loadingDetails ? (
  442. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  443. <CircularProgress />
  444. </Box>
  445. ) : (
  446. <>
  447. <TablePagination
  448. component="div"
  449. count={total}
  450. page={page}
  451. onPageChange={handleChangePage}
  452. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  453. onRowsPerPageChange={handleChangeRowsPerPage}
  454. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  455. labelRowsPerPage={t("Rows per page")}
  456. />
  457. <TableContainer component={Paper}>
  458. <Table>
  459. <TableHead>
  460. <TableRow>
  461. <TableCell>{t("Warehouse Location")}</TableCell>
  462. <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
  463. <TableCell>{t("UOM")}</TableCell>
  464. <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
  465. <TableCell>{t("Remark")}</TableCell>
  466. <TableCell>{t("Record Status")}</TableCell>
  467. <TableCell>{t("Action")}</TableCell>
  468. </TableRow>
  469. </TableHead>
  470. <TableBody>
  471. {filteredDetails.length === 0 ? (
  472. <TableRow>
  473. <TableCell colSpan={7} align="center">
  474. <Typography variant="body2" color="text.secondary">
  475. {t("No data")}
  476. </Typography>
  477. </TableCell>
  478. </TableRow>
  479. ) : (
  480. filteredDetails.map((detail) => {
  481. // const submitDisabled = isSubmitDisabled(detail);
  482. const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0;
  483. const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; // 改为 >= 0,允许0值
  484. const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first");
  485. return (
  486. <TableRow key={detail.id}>
  487. <TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
  488. <TableCell sx={{
  489. maxWidth: 150,
  490. wordBreak: 'break-word',
  491. whiteSpace: 'normal',
  492. lineHeight: 1.5
  493. }}>
  494. <Stack spacing={0.5}>
  495. <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
  496. <Box>{detail.lotNo || "-"}</Box>
  497. <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
  498. </Stack>
  499. </TableCell>
  500. <TableCell>{detail.uom || "-"}</TableCell>
  501. <TableCell sx={{ minWidth: 300 }}>
  502. {detail.finalQty != null ? (
  503. <Stack spacing={0.5}>
  504. {(() => {
  505. // 若有 bookQty(盤點當時帳面),用它來算差異;否則用 availableQty
  506. const bookQtyToUse = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
  507. const finalDifference = (detail.finalQty || 0) - bookQtyToUse;
  508. const differenceColor = detail.stockTakeRecordStatus === "completed"
  509. ? 'text.secondary'
  510. : finalDifference !== 0
  511. ? 'error.main'
  512. : 'success.main';
  513. return (
  514. <Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
  515. {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(bookQtyToUse)} = {formatNumber(finalDifference)}
  516. </Typography>
  517. );
  518. })()}
  519. </Stack>
  520. ) : (
  521. <Stack spacing={1}>
  522. {hasFirst && (
  523. <Stack direction="row" spacing={1} alignItems="center">
  524. <Radio
  525. size="small"
  526. checked={selection === "first"}
  527. onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })}
  528. />
  529. <Typography variant="body2">
  530. {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)}
  531. </Typography>
  532. </Stack>
  533. )}
  534. {hasSecond && (
  535. <Stack direction="row" spacing={1} alignItems="center">
  536. <Radio
  537. size="small"
  538. checked={selection === "second"}
  539. onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })}
  540. />
  541. <Typography variant="body2">
  542. {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)}
  543. </Typography>
  544. </Stack>
  545. )}
  546. {hasSecond && (
  547. <>
  548. <Stack direction="row" spacing={1} alignItems="center">
  549. <Radio
  550. size="small"
  551. checked={selection === "approver"}
  552. onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "approver" })}
  553. />
  554. <Typography variant="body2">{t("Approver Input")}:</Typography>
  555. <TextField
  556. size="small"
  557. type="number"
  558. value={approverQty[detail.id] || ""}
  559. onFocus={() => {
  560. setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: false }));
  561. if (selection !== "approver") {
  562. setQtySelection({ ...qtySelection, [detail.id]: "approver" });
  563. }
  564. }}
  565. onBlur={() =>
  566. setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: true }))
  567. }
  568. onChange={(e) => setApproverQty({ ...approverQty, [detail.id]: e.target.value })}
  569. InputProps={{
  570. endAdornment: stockTakeQtyEndAdornment(detail.uomShortDesc),
  571. }}
  572. sx={{
  573. width: 168,
  574. minWidth: 168,
  575. '& .MuiInputBase-input': {
  576. height: '1.4375em',
  577. padding: '4px 8px'
  578. }
  579. }}
  580. placeholder={t("Stock Take Qty") }
  581. />
  582. <TextField
  583. size="small"
  584. type="number"
  585. value={approverBadQty[detail.id] || ""}
  586. onChange={(e) => setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })}
  587. sx={{
  588. width: 130,
  589. minWidth: 130,
  590. '& .MuiInputBase-input': {
  591. height: '1.4375em',
  592. padding: '4px 8px'
  593. }
  594. }}
  595. placeholder={t("Bad Qty")}
  596. disabled={selection !== "approver"}
  597. />
  598. <Typography variant="body2">
  599. = {formatNumber(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
  600. </Typography>
  601. </Stack>
  602. <StockTakeQtyGapHint
  603. open={!!gapCheckOpen[`${detail.id}:approver`]}
  604. entered={approverQty[detail.id] || ""}
  605. currentQty={stockTakeHiddenOnHand(detail)}
  606. threshold={qtyGapWarnPercent}
  607. />
  608. </>
  609. )}
  610. {(() => {
  611. let selectedQty = 0;
  612. if (selection === "first") {
  613. selectedQty = detail.firstStockTakeQty || 0;
  614. } else if (selection === "second") {
  615. selectedQty = detail.secondStockTakeQty || 0;
  616. } else if (selection === "approver") {
  617. selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0;
  618. }
  619. const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
  620. const difference = selectedQty - bookQty;
  621. const differenceColor = detail.stockTakeRecordStatus === "completed"
  622. ? 'text.secondary'
  623. : difference !== 0
  624. ? 'error.main'
  625. : 'success.main';
  626. return (
  627. <Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
  628. {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)}
  629. </Typography>
  630. );
  631. })()}
  632. </Stack>
  633. )}
  634. </TableCell>
  635. <TableCell>
  636. <Typography variant="body2">
  637. {detail.remarks || "-"}
  638. </Typography>
  639. </TableCell>
  640. <TableCell>
  641. {detail.stockTakeRecordStatus === "completed" ? (
  642. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
  643. ) : detail.stockTakeRecordStatus === "pass" ? (
  644. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" />
  645. ) : detail.stockTakeRecordStatus === "notMatch" ? (
  646. <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
  647. ) : (
  648. <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
  649. )}
  650. </TableCell>
  651. <TableCell>
  652. {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && (
  653. <Box>
  654. <Button
  655. size="small"
  656. variant="outlined"
  657. color="warning"
  658. onClick={() => handleUpdateStatusToNotMatch(detail)}
  659. disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"||hasSecond}
  660. >
  661. {t("ReStockTake")}
  662. </Button>
  663. </Box>
  664. )}
  665. <br/>
  666. {detail.finalQty == null && (
  667. <Box>
  668. <Button
  669. size="small"
  670. variant="contained"
  671. onClick={() => handleSaveApproverStockTake(detail)}
  672. //disabled={saving || submitDisabled || detail.stockTakeRecordStatus === "completed"}
  673. >
  674. {t("Save")}
  675. </Button>
  676. </Box>
  677. )}
  678. </TableCell>
  679. </TableRow>
  680. );
  681. })
  682. )}
  683. </TableBody>
  684. </Table>
  685. </TableContainer>
  686. <TablePagination
  687. component="div"
  688. count={total}
  689. page={page}
  690. onPageChange={handleChangePage}
  691. rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
  692. onRowsPerPageChange={handleChangeRowsPerPage}
  693. rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
  694. labelRowsPerPage={t("Rows per page")}
  695. />
  696. </>
  697. )}
  698. </Box>
  699. );
  700. };
  701. export default ApproverStockTake;