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

ApproverStockTake.tsx 26 KiB

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