| @@ -40,6 +40,7 @@ export interface InventoryLotDetailResponse { | |||
| approverQty: number | null; | |||
| approverBadQty: number | null; | |||
| finalQty: number | null; | |||
| bookQty: number | null; | |||
| } | |||
| export const getInventoryLotDetailsBySection = async ( | |||
| @@ -207,6 +208,7 @@ export interface BatchSaveApproverStockTakeRecordRequest { | |||
| stockTakeId: number; | |||
| stockTakeSection: string; | |||
| approverId: number; | |||
| variancePercentTolerance?: number | null; | |||
| } | |||
| export interface BatchSaveApproverStockTakeRecordResponse { | |||
| @@ -312,7 +314,10 @@ export const getInventoryLotDetailsBySectionNotMatch = async ( | |||
| ); | |||
| return response; | |||
| } | |||
| export interface SearchStockTransactionResult { | |||
| records: StockTransactionResponse[]; | |||
| total: number; | |||
| } | |||
| export interface SearchStockTransactionRequest { | |||
| startDate: string | null; | |||
| endDate: string | null; | |||
| @@ -345,7 +350,6 @@ export interface StockTransactionListResponse { | |||
| } | |||
| export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => { | |||
| // 构建查询字符串 | |||
| const params = new URLSearchParams(); | |||
| if (request.itemCode) params.append("itemCode", request.itemCode); | |||
| @@ -366,7 +370,10 @@ export const searchStockTransactions = cache(async (request: SearchStockTransact | |||
| next: { tags: ["Stock Transaction List"] }, | |||
| } | |||
| ); | |||
| // 确保返回正确的格式 | |||
| return response?.records || []; | |||
| // 回傳 records 與 total,供分頁正確顯示 | |||
| return { | |||
| records: response?.records || [], | |||
| total: response?.total ?? 0, | |||
| }; | |||
| }); | |||
| @@ -68,6 +68,7 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||
| interface Props extends CommonProps { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| const QcStockInModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| @@ -94,6 +95,10 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| () => `qcStockInModal_selectedPrinterId_${session?.id ?? "guest"}`, | |||
| [session?.id], | |||
| ); | |||
| const labelPrinterCombo = useMemo( | |||
| () => (printerCombo || []).filter((p) => p.type === "Label"), | |||
| [printerCombo], | |||
| ); | |||
| const getDefaultPrinter = useMemo(() => { | |||
| if (!printerCombo.length) return undefined; | |||
| if (typeof window === "undefined") return printerCombo[0]; | |||
| @@ -102,7 +107,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| const matched = savedId ? printerCombo.find(p => p.id === Number(savedId)) : undefined; | |||
| return matched ?? printerCombo[0]; | |||
| }, [printerCombo, printerStorageKey]); | |||
| const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); | |||
| const [selectedPrinter, setSelectedPrinter] = useState(labelPrinterCombo[0]); | |||
| const [printQty, setPrintQty] = useState(1); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| @@ -504,6 +509,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| // Put away model | |||
| const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({}) | |||
| const [pafRowSelectionModel, setPafRowSelectionModel] = useState<GridRowSelectionModel>([]) | |||
| const pafSubmitDisable = useMemo(() => { | |||
| return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit) | |||
| }, [pafRowModesModel]) | |||
| @@ -749,21 +755,25 @@ const printQrcode = useCallback( | |||
| {tabIndex == 1 && ( | |||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo} | |||
| defaultValue={selectedPrinter} | |||
| onChange={(event, value) => { | |||
| setSelectedPrinter(value) | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| variant="outlined" | |||
| label={t("Printer")} | |||
| sx={{ width: 300}} | |||
| /> | |||
| )} | |||
| /> | |||
| disableClearable | |||
| options={labelPrinterCombo} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => { | |||
| if (newValue) setSelectedPrinter(newValue); | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| variant="outlined" | |||
| label={t("Printer")} | |||
| sx={{ width: 300 }} | |||
| inputProps={{ ...params.inputProps, readOnly: true }} | |||
| /> | |||
| )} | |||
| /> | |||
| <TextField | |||
| variant="outlined" | |||
| label={t("Print Qty")} | |||
| @@ -134,7 +134,7 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => { | |||
| // 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环) | |||
| useEffect(() => { | |||
| setFilteredList(processedData); | |||
| setTotalCount(processedData.length); | |||
| // 只在初始加载时设置 pageSize | |||
| if (isInitialMount.current && processedData.length > 0) { | |||
| setPageSize("all"); | |||
| @@ -146,55 +146,53 @@ const SearchPage: React.FC<Props> = ({ dataList: initialDataList }) => { | |||
| // API 调用函数(参考 PoSearch 的实现) | |||
| // API 调用函数(参考 PoSearch 的实现) | |||
| const newPageFetch = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, any>, | |||
| ) => { | |||
| setLoading(true); | |||
| try { | |||
| // 处理空字符串,转换为 null | |||
| const itemCode = filterArgs.itemCode?.trim() || null; | |||
| const itemName = filterArgs.itemName?.trim() || null; | |||
| // 验证:至少需要 itemCode 或 itemName | |||
| if (!itemCode && !itemName) { | |||
| console.warn("Search requires at least itemCode or itemName"); | |||
| const newPageFetch = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, any>, | |||
| ) => { | |||
| setLoading(true); | |||
| try { | |||
| const itemCode = filterArgs.itemCode?.trim() || null; | |||
| const itemName = filterArgs.itemName?.trim() || null; | |||
| if (!itemCode && !itemName) { | |||
| console.warn("Search requires at least itemCode or itemName"); | |||
| setDataList([]); | |||
| setTotalCount(0); | |||
| return; | |||
| } | |||
| const params: SearchStockTransactionRequest = { | |||
| itemCode: itemCode, | |||
| itemName: itemName, | |||
| type: filterArgs.type?.trim() || null, | |||
| startDate: filterArgs.startDate || null, | |||
| endDate: filterArgs.endDate || null, | |||
| pageNum: pagingController.pageNum - 1 || 0, | |||
| pageSize: pagingController.pageSize || 100, | |||
| }; | |||
| const res = await searchStockTransactions(params); | |||
| if (res && typeof res === 'object' && Array.isArray(res.records)) { | |||
| setDataList(res.records); | |||
| setTotalCount(res.total ?? res.records.length); | |||
| } else { | |||
| console.error("Invalid response format:", res); | |||
| setDataList([]); | |||
| setTotalCount(0); | |||
| } | |||
| } catch (error) { | |||
| console.error("Fetch error:", error); | |||
| setDataList([]); | |||
| setTotalCount(0); | |||
| return; | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| const params: SearchStockTransactionRequest = { | |||
| itemCode: itemCode, | |||
| itemName: itemName, | |||
| type: filterArgs.type?.trim() || null, | |||
| startDate: filterArgs.startDate || null, | |||
| endDate: filterArgs.endDate || null, | |||
| pageNum: pagingController.pageNum - 1 || 0, | |||
| pageSize: pagingController.pageSize || 100, | |||
| }; | |||
| console.log("Search params:", params); // 添加调试日志 | |||
| const res = await searchStockTransactions(params); | |||
| console.log("Search response:", res); // 添加调试日志 | |||
| if (res && Array.isArray(res)) { | |||
| setDataList(res); | |||
| } else { | |||
| console.error("Invalid response format:", res); | |||
| setDataList([]); | |||
| } | |||
| } catch (error) { | |||
| console.error("Fetch error:", error); | |||
| setDataList([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| }, | |||
| [], | |||
| ); | |||
| // 使用 useRef 来存储上一次的值,避免不必要的 API 调用 | |||
| const prevPagingControllerRef = useRef(pagingController); | |||
| @@ -240,13 +238,13 @@ const newPageFetch = useCallback( | |||
| const newSize = parseInt(event.target.value, 10); | |||
| if (newSize === -1) { | |||
| setPageSize("all"); | |||
| setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 })); | |||
| setPagingController(prev => ({ ...prev, pageSize: 100, pageNum: 1 })); // 用 100 觸發後端回傳全部 | |||
| } else if (!isNaN(newSize)) { | |||
| setPageSize(newSize); | |||
| setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 })); | |||
| } | |||
| setPage(0); | |||
| }, [filteredList.length]); | |||
| }, []); | |||
| const searchCriteria: Criterion<string>[] = useMemo( | |||
| () => [ | |||
| @@ -390,29 +388,25 @@ const newPageFetch = useCallback( | |||
| setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||
| }, []); | |||
| // 计算实际显示的 items(分页) | |||
| const paginatedItems = useMemo(() => { | |||
| if (pageSize === "all") { | |||
| return filteredList; | |||
| } | |||
| const actualPageSize = typeof pageSize === 'number' ? pageSize : 10; | |||
| const startIndex = page * actualPageSize; | |||
| const endIndex = startIndex + actualPageSize; | |||
| return filteredList.slice(startIndex, endIndex); | |||
| }, [filteredList, page, pageSize]); | |||
| return filteredList; | |||
| }, [filteredList, pageSize]); | |||
| // 计算传递给 SearchResults 的 pageSize(确保在选项中) | |||
| const actualPageSizeForTable = useMemo(() => { | |||
| if (pageSize === "all") { | |||
| return filteredList.length; | |||
| return totalCount > 0 ? totalCount : filteredList.length; | |||
| } | |||
| const size = typeof pageSize === 'number' ? pageSize : 10; | |||
| // 如果 size 不在标准选项中,使用 "all" 模式 | |||
| if (![10, 25, 100].includes(size)) { | |||
| return filteredList.length; | |||
| return size; | |||
| } | |||
| return size; | |||
| }, [pageSize, filteredList.length]); | |||
| }, [pageSize, filteredList.length, totalCount]); | |||
| return ( | |||
| <> | |||
| @@ -56,8 +56,8 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); | |||
| const [loadingDetails, setLoadingDetails] = useState(false); | |||
| const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false); | |||
| const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(true); | |||
| const [variancePercentTolerance, setVariancePercentTolerance] = useState<string>("5"); | |||
| // 每个记录的选择状态,key 为 detail.id | |||
| const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({}); | |||
| const [approverQty, setApproverQty] = useState<Record<number, string>>({}); | |||
| @@ -71,7 +71,17 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const handleBatchSubmitAllRef = useRef<() => Promise<void>>(); | |||
| const isWithinVarianceTolerance = useCallback(( | |||
| difference: number, | |||
| bookQty: number, | |||
| percentStr: string | |||
| ): boolean => { | |||
| const percent = parseFloat(percentStr || "0"); | |||
| if (isNaN(percent) || percent < 0) return true; // 无效输入时视为全部通过 | |||
| if (bookQty === 0) return difference === 0; | |||
| const threshold = Math.abs(bookQty) * (percent / 100); | |||
| return Math.abs(difference) <= threshold; | |||
| }, []); | |||
| const handleChangePage = useCallback((event: unknown, newPage: number) => { | |||
| setPage(newPage); | |||
| }, []); | |||
| @@ -133,7 +143,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0; | |||
| } | |||
| const bookQty = detail.availableQty || 0; | |||
| const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); | |||
| return selectedQty - bookQty; | |||
| }, [approverQty, approverBadQty]); | |||
| @@ -159,16 +169,29 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| // 4. 添加过滤逻辑(在渲染表格之前) | |||
| const filteredDetails = useMemo(() => { | |||
| if (!showOnlyWithDifference) { | |||
| return inventoryLotDetails; | |||
| let result = inventoryLotDetails; | |||
| if (showOnlyWithDifference) { | |||
| const percent = parseFloat(variancePercentTolerance || "0"); | |||
| const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent; | |||
| result = result.filter(detail => { | |||
| // 已完成項目一律顯示 | |||
| if (detail.finalQty != null || detail.stockTakeRecordStatus === "completed") { | |||
| return true; | |||
| } | |||
| const selection = qtySelection[detail.id] ?? | |||
| (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); | |||
| const difference = calculateDifference(detail, selection); | |||
| const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); | |||
| if (bookQty === 0) return difference !== 0; | |||
| const threshold = Math.abs(bookQty) * (thresholdPercent / 100); | |||
| return Math.abs(difference) > threshold; | |||
| }); | |||
| } | |||
| return inventoryLotDetails.filter(detail => { | |||
| const selection = qtySelection[detail.id] || (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); | |||
| const difference = calculateDifference(detail, selection); | |||
| return difference !== 0; | |||
| }); | |||
| }, [inventoryLotDetails, showOnlyWithDifference, qtySelection, calculateDifference]); | |||
| return result; | |||
| }, [inventoryLotDetails, showOnlyWithDifference, variancePercentTolerance, qtySelection, calculateDifference]); | |||
| const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { | |||
| if (!selectedSession || !currentUserId) { | |||
| @@ -231,7 +254,22 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| onSnackbar(t("Approver stock take record saved successfully"), "success"); | |||
| await loadDetails(page, pageSize); | |||
| // 計算最終數量(合格數) | |||
| const goodQty = finalQty - finalBadQty; | |||
| setInventoryLotDetails((prev) => | |||
| prev.map((d) => | |||
| d.id === detail.id | |||
| ? { | |||
| ...d, | |||
| finalQty: goodQty, | |||
| approverQty: selection === "approver" ? finalQty : d.approverQty, | |||
| approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty, | |||
| stockTakeRecordStatus: "completed", | |||
| } | |||
| : d | |||
| ) | |||
| ); | |||
| } catch (e: any) { | |||
| console.error("Save approver stock take record error:", e); | |||
| let errorMessage = t("Failed to save approver stock take record"); | |||
| @@ -264,6 +302,11 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); | |||
| onSnackbar(t("Stock take record status updated to not match"), "success"); | |||
| setInventoryLotDetails((prev) => | |||
| prev.map((d) => | |||
| d.id === detail.id ? { ...d, stockTakeRecordStatus: "notMatch" } : d | |||
| ) | |||
| ); | |||
| } catch (e: any) { | |||
| console.error("Update stock take record status error:", e); | |||
| let errorMessage = t("Failed to update stock take record status"); | |||
| @@ -284,17 +327,9 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| setUpdatingStatus(false); | |||
| // Reload after status update - the useEffect will handle it with current page/pageSize | |||
| // Or explicitly reload: | |||
| setPage((currentPage) => { | |||
| setPageSize((currentPageSize) => { | |||
| setTimeout(() => { | |||
| loadDetails(currentPage, currentPageSize); | |||
| }, 0); | |||
| return currentPageSize; | |||
| }); | |||
| return currentPage; | |||
| }); | |||
| } | |||
| }, [selectedSession, t, onSnackbar, loadDetails]); | |||
| }, [selectedSession, t, onSnackbar, ]); | |||
| const handleBatchSubmitAll = useCallback(async () => { | |||
| if (!selectedSession || !currentUserId) { | |||
| @@ -309,6 +344,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| stockTakeId: selectedSession.stockTakeId, | |||
| stockTakeSection: selectedSession.stockTakeSession, | |||
| approverId: currentUserId, | |||
| variancePercentTolerance: parseFloat(variancePercentTolerance || "0") || undefined, | |||
| }; | |||
| const result = await batchSaveApproverStockTakeRecords(request); | |||
| @@ -349,10 +385,10 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| }, [handleBatchSubmitAll]); | |||
| const formatNumber = (num: number | null | undefined): string => { | |||
| if (num == null) return "0.00"; | |||
| if (num == null) return "0"; | |||
| return num.toLocaleString('en-US', { | |||
| minimumFractionDigits: 2, | |||
| maximumFractionDigits: 2 | |||
| minimumFractionDigits: 0, | |||
| maximumFractionDigits: 0 | |||
| }); | |||
| }; | |||
| @@ -411,25 +447,30 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} alignItems="center"> | |||
| <Button | |||
| variant={showOnlyWithDifference ? "contained" : "outlined"} | |||
| color="primary" | |||
| onClick={() => setShowOnlyWithDifference(!showOnlyWithDifference)} | |||
| startIcon={ | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={variancePercentTolerance} | |||
| onChange={(e) => setVariancePercentTolerance(e.target.value)} | |||
| label={t("Variance %")} | |||
| sx={{ width: 100 }} | |||
| inputProps={{ min: 0, max: 100, step: 0.1 }} | |||
| /> | |||
| {/* | |||
| <FormControlLabel | |||
| control={ | |||
| <Checkbox | |||
| checked={showOnlyWithDifference} | |||
| onChange={(e) => setShowOnlyWithDifference(e.target.checked)} | |||
| sx={{ p: 0, pointerEvents: 'none' }} | |||
| /> | |||
| } | |||
| sx={{ textTransform: 'none' }} | |||
| > | |||
| {t("Only Variance")} | |||
| label={t("Only Variance")} | |||
| /> | |||
| */} | |||
| <Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}> | |||
| {t("Batch Save All")} | |||
| </Button> | |||
| <Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}> | |||
| {t("Batch Save All")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </Stack> | |||
| {loadingDetails ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| @@ -454,9 +495,10 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| <TableRow> | |||
| <TableCell>{t("Warehouse Location")}</TableCell> | |||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| <TableCell>{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -492,25 +534,27 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| {detail.finalQty != null ? ( | |||
| <Stack spacing={0.5}> | |||
| {(() => { | |||
| const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0); | |||
| const differenceColor = finalDifference > 0 | |||
| ? 'error.main' | |||
| : finalDifference < 0 | |||
| ? 'error.main' | |||
| : 'success.main'; | |||
| return ( | |||
| <Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}> | |||
| {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)} | |||
| </Typography> | |||
| ); | |||
| })()} | |||
| </Stack> | |||
| {detail.finalQty != null ? ( | |||
| <Stack spacing={0.5}> | |||
| {(() => { | |||
| // 若有 bookQty(盤點當時帳面),用它來算差異;否則用 availableQty | |||
| const bookQtyToUse = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); | |||
| const finalDifference = (detail.finalQty || 0) - bookQtyToUse; | |||
| const differenceColor = detail.stockTakeRecordStatus === "completed" | |||
| ? 'text.secondary' | |||
| : finalDifference !== 0 | |||
| ? 'error.main' | |||
| : 'success.main'; | |||
| return ( | |||
| <Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}> | |||
| {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(bookQtyToUse)} = {formatNumber(finalDifference)} | |||
| </Typography> | |||
| ); | |||
| })()} | |||
| </Stack> | |||
| ) : ( | |||
| <Stack spacing={1}> | |||
| {hasFirst && ( | |||
| @@ -581,7 +625,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| disabled={selection !== "approver"} | |||
| /> | |||
| <Typography variant="body2"> | |||
| ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} | |||
| = {formatNumber(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} | |||
| </Typography> | |||
| </Stack> | |||
| )} | |||
| @@ -597,12 +641,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; | |||
| } | |||
| const bookQty = detail.availableQty || 0; | |||
| const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); | |||
| const difference = selectedQty - bookQty; | |||
| const differenceColor = difference > 0 | |||
| ? 'error.main' | |||
| : difference < 0 | |||
| ? 'error.main' | |||
| const differenceColor = detail.stockTakeRecordStatus === "completed" | |||
| ? 'text.secondary' | |||
| : difference !== 0 | |||
| ? 'error.main' | |||
| : 'success.main'; | |||
| return ( | |||
| @@ -621,11 +665,13 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| <TableCell> | |||
| {detail.stockTakeRecordStatus === "pass" ? ( | |||
| {detail.stockTakeRecordStatus === "completed" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||
| ) : detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" /> | |||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" /> | |||
| ) : ( | |||
| @@ -21,7 +21,6 @@ import { useState, useCallback, useEffect, useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| AllPickedStockTakeListReponse, | |||
| getInventoryLotDetailsBySection, | |||
| InventoryLotDetailResponse, | |||
| saveStockTakeRecord, | |||
| SaveStockTakeRecordRequest, | |||
| @@ -51,13 +50,13 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); | |||
| const [loadingDetails, setLoadingDetails] = useState(false); | |||
| // 编辑状态 | |||
| const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null); | |||
| const [firstQty, setFirstQty] = useState<string>(""); | |||
| const [secondQty, setSecondQty] = useState<string>(""); | |||
| const [firstBadQty, setFirstBadQty] = useState<string>(""); | |||
| const [secondBadQty, setSecondBadQty] = useState<string>(""); | |||
| const [remark, setRemark] = useState<string>(""); | |||
| const [recordInputs, setRecordInputs] = useState<Record<number, { | |||
| firstQty: string; | |||
| secondQty: string; | |||
| firstBadQty: string; | |||
| secondBadQty: string; | |||
| remark: string; | |||
| }>>({}); | |||
| const [saving, setSaving] = useState(false); | |||
| const [batchSaving, setBatchSaving] = useState(false); | |||
| const [shortcutInput, setShortcutInput] = useState<string>(""); | |||
| @@ -115,28 +114,36 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| } | |||
| }, [selectedSession, total]); | |||
| useEffect(() => { | |||
| const inputs: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {}; | |||
| inventoryLotDetails.forEach((detail) => { | |||
| const firstTotal = detail.firstStockTakeQty != null | |||
| ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString() | |||
| : ""; | |||
| const secondTotal = detail.secondStockTakeQty != null | |||
| ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString() | |||
| : ""; | |||
| inputs[detail.id] = { | |||
| firstQty: firstTotal, | |||
| secondQty: secondTotal, | |||
| firstBadQty: detail.firstBadQty?.toString() || "", | |||
| secondBadQty: detail.secondBadQty?.toString() || "", | |||
| remark: detail.remarks || "", | |||
| }; | |||
| }); | |||
| setRecordInputs(inputs); | |||
| }, [inventoryLotDetails]); | |||
| useEffect(() => { | |||
| loadDetails(page, pageSize); | |||
| }, [page, pageSize, loadDetails]); | |||
| const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { | |||
| setEditingRecord(detail); | |||
| setFirstQty(detail.firstStockTakeQty?.toString() || ""); | |||
| setSecondQty(detail.secondStockTakeQty?.toString() || ""); | |||
| setFirstBadQty(detail.firstBadQty?.toString() || ""); | |||
| setSecondBadQty(detail.secondBadQty?.toString() || ""); | |||
| setRemark(detail.remarks || ""); | |||
| }, []); | |||
| const handleCancelEdit = useCallback(() => { | |||
| setEditingRecord(null); | |||
| setFirstQty(""); | |||
| setSecondQty(""); | |||
| setFirstBadQty(""); | |||
| setSecondBadQty(""); | |||
| setRemark(""); | |||
| }, []); | |||
| const formatNumber = (num: number | null | undefined): string => { | |||
| if (num == null || Number.isNaN(num)) return "0"; | |||
| return num.toLocaleString("en-US", { | |||
| minimumFractionDigits: 0, | |||
| maximumFractionDigits: 0, | |||
| }); | |||
| }; | |||
| const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { | |||
| if (!selectedSession || !currentUserId) { | |||
| return; | |||
| @@ -145,41 +152,69 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||
| const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||
| const qty = isFirstSubmit ? firstQty : secondQty; | |||
| const badQty = isFirstSubmit ? firstBadQty : secondBadQty; | |||
| if (!qty || !badQty) { | |||
| // 用戶輸入為 total 和 bad,需計算 available = total - bad(與 PickerStockTake 一致) | |||
| const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; | |||
| const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; | |||
| if (!totalQtyStr) { | |||
| onSnackbar( | |||
| isFirstSubmit | |||
| ? t("Please enter QTY and Bad QTY") | |||
| : t("Please enter Second QTY and Bad QTY"), | |||
| ? t("Please enter QTY") | |||
| : t("Please enter Second QTY"), | |||
| "error" | |||
| ); | |||
| return; | |||
| } | |||
| const totalQty = parseFloat(totalQtyStr); | |||
| const badQty = parseFloat(badQtyStr || "0") || 0; | |||
| if (Number.isNaN(totalQty)) { | |||
| onSnackbar(t("Invalid QTY"), "error"); | |||
| return; | |||
| } | |||
| const availableQty = totalQty - badQty; | |||
| if (availableQty < 0) { | |||
| onSnackbar(t("Available QTY cannot be negative"), "error"); | |||
| return; | |||
| } | |||
| setSaving(true); | |||
| try { | |||
| const request: SaveStockTakeRecordRequest = { | |||
| stockTakeRecordId: detail.stockTakeRecordId || null, | |||
| inventoryLotLineId: detail.id, | |||
| qty: parseFloat(qty), | |||
| badQty: parseFloat(badQty), | |||
| remark: isSecondSubmit ? (remark || null) : null, | |||
| qty: availableQty, | |||
| badQty: badQty, | |||
| remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, | |||
| }; | |||
| console.log('handleSaveStockTake: request:', request); | |||
| console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); | |||
| console.log('handleSaveStockTake: currentUserId:', currentUserId); | |||
| await saveStockTakeRecord( | |||
| const result = await saveStockTakeRecord( | |||
| request, | |||
| selectedSession.stockTakeId, | |||
| currentUserId | |||
| ); | |||
| onSnackbar(t("Stock take record saved successfully"), "success"); | |||
| handleCancelEdit(); | |||
| await loadDetails(page, pageSize); | |||
| const savedId = result?.id ?? detail.stockTakeRecordId; | |||
| setInventoryLotDetails((prev) => | |||
| prev.map((d) => | |||
| d.id === detail.id | |||
| ? { | |||
| ...d, | |||
| stockTakeRecordId: savedId ?? d.stockTakeRecordId, | |||
| firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty, | |||
| firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null, | |||
| secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty, | |||
| secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null, | |||
| remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, | |||
| stockTakeRecordStatus: "pass", | |||
| } | |||
| : d | |||
| ) | |||
| ); | |||
| } catch (e: any) { | |||
| console.error("Save stock take record error:", e); | |||
| let errorMessage = t("Failed to save stock take record"); | |||
| @@ -199,15 +234,13 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| } finally { | |||
| setSaving(false); | |||
| } | |||
| }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); | |||
| }, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); | |||
| const handleBatchSubmitAll = useCallback(async () => { | |||
| if (!selectedSession || !currentUserId) { | |||
| console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); | |||
| return; | |||
| } | |||
| console.log('handleBatchSubmitAll: Starting batch save...'); | |||
| setBatchSaving(true); | |||
| try { | |||
| const request: BatchSaveStockTakeRecordRequest = { | |||
| @@ -217,7 +250,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }; | |||
| const result = await batchSaveStockTakeRecords(request); | |||
| console.log('handleBatchSubmitAll: Result:', result); | |||
| onSnackbar( | |||
| t("Batch save completed: {{success}} success, {{errors}} errors", { | |||
| @@ -273,31 +305,19 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| const newInput = prev + e.key; | |||
| if (newInput === '{2fitestall}') { | |||
| console.log('✅ Shortcut {2fitestall} detected!'); | |||
| setTimeout(() => { | |||
| if (handleBatchSubmitAllRef.current) { | |||
| console.log('Calling handleBatchSubmitAll...'); | |||
| handleBatchSubmitAllRef.current().catch(err => { | |||
| console.error('Error in handleBatchSubmitAll:', err); | |||
| }); | |||
| } else { | |||
| console.error('handleBatchSubmitAllRef.current is null'); | |||
| } | |||
| }, 0); | |||
| return ""; | |||
| } | |||
| if (newInput.length > 15) { | |||
| return ""; | |||
| } | |||
| if (newInput.length > 0 && !newInput.startsWith('{')) { | |||
| return ""; | |||
| } | |||
| if (newInput.length > 5 && !newInput.startsWith('{2fi')) { | |||
| return ""; | |||
| } | |||
| if (newInput.length > 15) return ""; | |||
| if (newInput.length > 0 && !newInput.startsWith('{')) return ""; | |||
| if (newInput.length > 5 && !newInput.startsWith('{2fi')) return ""; | |||
| return newInput; | |||
| }); | |||
| @@ -315,11 +335,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }, []); | |||
| const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | |||
| if (detail.stockTakeRecordStatus === "pass") { | |||
| if (selectedSession?.status?.toLowerCase() === "completed") { | |||
| return true; | |||
| } | |||
| const recordStatus = detail.stockTakeRecordStatus?.toLowerCase(); | |||
| if (recordStatus === "pass" || recordStatus === "completed") { | |||
| return true; | |||
| } | |||
| return false; | |||
| }, []); | |||
| }, [selectedSession?.status]); | |||
| const uniqueWarehouses = Array.from( | |||
| new Set( | |||
| @@ -328,6 +352,9 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| .filter(warehouse => warehouse && warehouse.trim() !== "") | |||
| ) | |||
| ).join(", "); | |||
| const defaultInputs = { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }; | |||
| return ( | |||
| <Box> | |||
| <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | |||
| @@ -339,42 +366,31 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||
| )} | |||
| </Typography> | |||
| {/* | |||
| {shortcutInput && ( | |||
| <Box sx={{ mb: 2, p: 1.5, bgcolor: 'info.light', borderRadius: 1, border: '1px solid', borderColor: 'info.main' }}> | |||
| <Typography variant="body2" color="info.dark" fontWeight={500}> | |||
| {t("Shortcut Input")}: <strong style={{ fontFamily: 'monospace', fontSize: '1.1em' }}>{shortcutInput}</strong> | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| */} | |||
| {loadingDetails ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <> | |||
| <TablePagination | |||
| component="div" | |||
| count={total} | |||
| page={page} | |||
| onPageChange={handleChangePage} | |||
| rowsPerPage={pageSize === "all" ? total : (pageSize as number)} | |||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||
| rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| /> | |||
| <TablePagination | |||
| component="div" | |||
| count={total} | |||
| page={page} | |||
| onPageChange={handleChangePage} | |||
| rowsPerPage={pageSize === "all" ? total : (pageSize as number)} | |||
| onRowsPerPageChange={handleChangeRowsPerPage} | |||
| rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| /> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableRow> | |||
| <TableCell>{t("Warehouse Location")}</TableCell> | |||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||
| <TableCell>{t("Qty")}</TableCell> | |||
| <TableCell>{t("Bad Qty")}</TableCell> | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| <TableCell>{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -382,7 +398,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| <TableBody> | |||
| {inventoryLotDetails.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={8} align="center"> | |||
| <TableCell colSpan={7} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data")} | |||
| </Typography> | |||
| @@ -390,99 +406,156 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| </TableRow> | |||
| ) : ( | |||
| inventoryLotDetails.map((detail) => { | |||
| const isEditing = editingRecord?.id === detail.id; | |||
| const submitDisabled = isSubmitDisabled(detail); | |||
| const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||
| const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||
| const inputs = recordInputs[detail.id] ?? defaultInputs; | |||
| return ( | |||
| <TableRow key={detail.id}> | |||
| <TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell> | |||
| <TableCell sx={{ | |||
| maxWidth: 150, | |||
| wordBreak: 'break-word', | |||
| whiteSpace: 'normal', | |||
| lineHeight: 1.5 | |||
| }}> | |||
| maxWidth: 150, | |||
| wordBreak: 'break-word', | |||
| whiteSpace: 'normal', | |||
| lineHeight: 1.5 | |||
| }}> | |||
| <Stack spacing={0.5}> | |||
| <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box> | |||
| <Box>{detail.lotNo || "-"}</Box> | |||
| <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Stack spacing={0.5}> | |||
| {isEditing && isFirstSubmit ? ( | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={firstQty} | |||
| onChange={(e) => setFirstQty(e.target.value)} | |||
| sx={{ width: 100 }} | |||
| /> | |||
| ) : detail.firstStockTakeQty ? ( | |||
| <Typography variant="body2"> | |||
| {t("First")}: {detail.firstStockTakeQty.toFixed(2)} | |||
| </Typography> | |||
| ) : null} | |||
| {isEditing && isSecondSubmit ? ( | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={secondQty} | |||
| onChange={(e) => setSecondQty(e.target.value)} | |||
| sx={{ width: 100 }} | |||
| /> | |||
| ) : detail.secondStockTakeQty ? ( | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| <Stack spacing={1}> | |||
| {/* First */} | |||
| {!submitDisabled && isFirstSubmit ? ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Typography variant="body2">{t("First")}:</Typography> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={inputs.firstQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstQty: val } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| "& .MuiInputBase-input": { | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={inputs.firstBadQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstBadQty: val } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| "& .MuiInputBase-input": { | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| <Typography variant="body2"> | |||
| = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))} | |||
| </Typography> | |||
| </Stack> | |||
| ) : detail.firstStockTakeQty != null ? ( | |||
| <Typography variant="body2"> | |||
| {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} | |||
| {t("First")}:{" "} | |||
| {formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "} | |||
| ({formatNumber(detail.firstBadQty ?? 0)}) ={" "} | |||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( | |||
| <Typography variant="body2" color="text.secondary"> | |||
| - | |||
| </Typography> | |||
| )} | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Stack spacing={0.5}> | |||
| {isEditing && isFirstSubmit ? ( | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={firstBadQty} | |||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||
| sx={{ width: 100 }} | |||
| /> | |||
| ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( | |||
| <Typography variant="body2"> | |||
| {t("First")}: {detail.firstBadQty.toFixed(2)} | |||
| </Typography> | |||
| ) : ( | |||
| <Typography variant="body2" sx={{ visibility: 'hidden' }}> | |||
| {t("First")}: 0.00 | |||
| </Typography> | |||
| )} | |||
| {isEditing && isSecondSubmit ? ( | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={secondBadQty} | |||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||
| sx={{ width: 100 }} | |||
| /> | |||
| ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( | |||
| {/* Second */} | |||
| {!submitDisabled && isSecondSubmit ? ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Typography variant="body2">{t("Second")}:</Typography> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={inputs.secondQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: val } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| "& .MuiInputBase-input": { | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={inputs.secondBadQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: val } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| "& .MuiInputBase-input": { | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| <Typography variant="body2"> | |||
| = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))} | |||
| </Typography> | |||
| </Stack> | |||
| ) : detail.secondStockTakeQty != null ? ( | |||
| <Typography variant="body2"> | |||
| {t("Second")}: {detail.secondBadQty.toFixed(2)} | |||
| {t("Second")}:{" "} | |||
| {formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "} | |||
| ({formatNumber(detail.secondBadQty ?? 0)}) ={" "} | |||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( | |||
| {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && ( | |||
| <Typography variant="body2" color="text.secondary"> | |||
| - | |||
| </Typography> | |||
| @@ -490,13 +563,16 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell sx={{ width: 180 }}> | |||
| {isEditing && isSecondSubmit ? ( | |||
| {!submitDisabled && isSecondSubmit ? ( | |||
| <> | |||
| <Typography variant="body2">{t("Remark")}</Typography> | |||
| <TextField | |||
| size="small" | |||
| value={remark} | |||
| onChange={(e) => setRemark(e.target.value)} | |||
| value={inputs.remark} | |||
| onChange={(e) => setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value } | |||
| }))} | |||
| sx={{ width: 150 }} | |||
| /> | |||
| </> | |||
| @@ -506,49 +582,30 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| </Typography> | |||
| )} | |||
| </TableCell> | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| <TableCell> | |||
| {detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" /> | |||
| ) : ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" /> | |||
| )} | |||
| </TableCell> | |||
| {detail.stockTakeRecordStatus === "completed" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||
| ) : detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="default" /> | |||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" /> | |||
| ) : ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" /> | |||
| )} | |||
| </TableCell> | |||
| <TableCell> | |||
| {isEditing ? ( | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => handleSaveStockTake(detail)} | |||
| disabled={saving || submitDisabled} | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| onClick={handleCancelEdit} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| ) : ( | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleStartEdit(detail)} | |||
| disabled={submitDisabled} | |||
| variant="contained" | |||
| onClick={() => handleSaveStockTake(detail)} | |||
| disabled={saving || submitDisabled } | |||
| > | |||
| {!detail.stockTakeRecordId | |||
| ? t("Input") | |||
| : detail.stockTakeRecordStatus === "notMatch" | |||
| ? t("Input") | |||
| : t("View")} | |||
| {t("Save")} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| @@ -55,13 +55,14 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); | |||
| const [loadingDetails, setLoadingDetails] = useState(false); | |||
| // 编辑状态 | |||
| const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null); | |||
| // firstQty / secondQty 保存的是 total = available + bad | |||
| const [firstQty, setFirstQty] = useState<string>(""); | |||
| const [secondQty, setSecondQty] = useState<string>(""); | |||
| const [firstBadQty, setFirstBadQty] = useState<string>(""); | |||
| const [secondBadQty, setSecondBadQty] = useState<string>(""); | |||
| const [recordInputs, setRecordInputs] = useState<Record<number, { | |||
| firstQty: string; | |||
| secondQty: string; | |||
| firstBadQty: string; | |||
| secondBadQty: string; | |||
| remark: string; | |||
| }>>({}); | |||
| const [savingRecordId, setSavingRecordId] = useState<number | null>(null); | |||
| const [remark, setRemark] = useState<string>(""); | |||
| const [saving, setSaving] = useState(false); | |||
| const [batchSaving, setBatchSaving] = useState(false); | |||
| @@ -91,7 +92,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| } | |||
| setPage(0); | |||
| }, []); | |||
| const loadDetails = useCallback(async (pageNum: number, size: number | string) => { | |||
| const loadDetails = useCallback(async ( | |||
| pageNum: number, | |||
| size: number | string, | |||
| options?: { silent?: boolean } | |||
| ) => { | |||
| console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber }); | |||
| setLoadingDetails(true); | |||
| try { | |||
| @@ -132,44 +137,34 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| setLoadingDetails(false); | |||
| } | |||
| }, [selectedSession, total]); | |||
| useEffect(() => { | |||
| loadDetails(page, pageSize); | |||
| }, [page, pageSize, loadDetails]); | |||
| const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { | |||
| setEditingRecord(detail); | |||
| // 编辑时,输入 total = qty + badQty | |||
| const firstTotal = | |||
| detail.firstStockTakeQty != null | |||
| const inputs: Record<number, { firstQty: string; secondQty: string; firstBadQty: string; secondBadQty: string; remark: string }> = {}; | |||
| inventoryLotDetails.forEach((detail) => { | |||
| const firstTotal = detail.firstStockTakeQty != null | |||
| ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString() | |||
| : ""; | |||
| const secondTotal = | |||
| detail.secondStockTakeQty != null | |||
| const secondTotal = detail.secondStockTakeQty != null | |||
| ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString() | |||
| : ""; | |||
| setFirstQty(firstTotal); | |||
| setSecondQty(secondTotal); | |||
| setFirstBadQty(detail.firstBadQty?.toString() || ""); | |||
| setSecondBadQty(detail.secondBadQty?.toString() || ""); | |||
| setRemark(detail.remarks || ""); | |||
| }, []); | |||
| const handleCancelEdit = useCallback(() => { | |||
| setEditingRecord(null); | |||
| setFirstQty(""); | |||
| setSecondQty(""); | |||
| setFirstBadQty(""); | |||
| setSecondBadQty(""); | |||
| setRemark(""); | |||
| }, []); | |||
| inputs[detail.id] = { | |||
| firstQty: firstTotal, | |||
| secondQty: secondTotal, | |||
| firstBadQty: detail.firstBadQty?.toString() || "", | |||
| secondBadQty: detail.secondBadQty?.toString() || "", | |||
| remark: detail.remarks || "", | |||
| }; | |||
| }); | |||
| setRecordInputs(inputs); | |||
| }, [inventoryLotDetails]); | |||
| useEffect(() => { | |||
| loadDetails(page, pageSize); | |||
| }, [page, pageSize, loadDetails]); | |||
| const formatNumber = (num: number | null | undefined): string => { | |||
| if (num == null || Number.isNaN(num)) return "0.00"; | |||
| if (num == null || Number.isNaN(num)) return "0"; | |||
| return num.toLocaleString("en-US", { | |||
| minimumFractionDigits: 2, | |||
| maximumFractionDigits: 2, | |||
| minimumFractionDigits: 0, | |||
| maximumFractionDigits: 0, | |||
| }); | |||
| }; | |||
| @@ -184,24 +179,25 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||
| // 现在用户输入的是 total 和 bad,需要算 available = total - bad | |||
| const totalQtyStr = isFirstSubmit ? firstQty : secondQty; | |||
| const badQtyStr = isFirstSubmit ? firstBadQty : secondBadQty; | |||
| if (!totalQtyStr || !badQtyStr) { | |||
| const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; | |||
| const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; | |||
| // 只檢查 totalQty,Bad Qty 未輸入時預設為 0 | |||
| if (!totalQtyStr) { | |||
| onSnackbar( | |||
| isFirstSubmit | |||
| ? t("Please enter QTY and Bad QTY") | |||
| : t("Please enter Second QTY and Bad QTY"), | |||
| ? t("Please enter QTY") | |||
| : t("Please enter Second QTY"), | |||
| "error" | |||
| ); | |||
| return; | |||
| } | |||
| const totalQty = parseFloat(totalQtyStr); | |||
| const badQty = parseFloat(badQtyStr); | |||
| if (Number.isNaN(totalQty) || Number.isNaN(badQty)) { | |||
| onSnackbar(t("Invalid QTY or Bad QTY"), "error"); | |||
| const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0 | |||
| if (Number.isNaN(totalQty)) { | |||
| onSnackbar(t("Invalid QTY"), "error"); | |||
| return; | |||
| } | |||
| @@ -219,7 +215,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| inventoryLotLineId: detail.id, | |||
| qty: availableQty, // 保存 available qty | |||
| badQty: badQty, // 保存 bad qty | |||
| remark: isSecondSubmit ? (remark || null) : null, | |||
| remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, | |||
| }; | |||
| console.log("handleSaveStockTake: request:", request); | |||
| console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId); | |||
| @@ -228,10 +224,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId); | |||
| onSnackbar(t("Stock take record saved successfully"), "success"); | |||
| handleCancelEdit(); | |||
| await loadDetails(page, pageSize); | |||
| //await loadDetails(page, pageSize, { silent: true }); | |||
| setInventoryLotDetails((prev) => | |||
| prev.map((d) => | |||
| d.id === detail.id | |||
| ? { | |||
| ...d, | |||
| stockTakeRecordId: d.stockTakeRecordId ?? null, // 首次儲存後可從 response 取得,此處先保留 | |||
| firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty, | |||
| firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null, | |||
| secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty, | |||
| secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null, | |||
| remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, | |||
| stockTakeRecordStatus: "pass", | |||
| } | |||
| : d | |||
| ) | |||
| ); | |||
| } catch (e: any) { | |||
| console.error("Save stock take record error:", e); | |||
| let errorMessage = t("Failed to save stock take record"); | |||
| @@ -254,18 +264,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }, | |||
| [ | |||
| selectedSession, | |||
| firstQty, | |||
| secondQty, | |||
| firstBadQty, | |||
| secondBadQty, | |||
| recordInputs, | |||
| remark, | |||
| handleCancelEdit, | |||
| t, | |||
| currentUserId, | |||
| onSnackbar, | |||
| loadDetails, | |||
| page, | |||
| pageSize, | |||
| ] | |||
| ); | |||
| @@ -387,11 +390,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }, []); | |||
| const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | |||
| if (detail.stockTakeRecordStatus === "pass") { | |||
| if (selectedSession?.status?.toLowerCase() === "completed") { | |||
| return true; | |||
| } | |||
| const recordStatus = detail.stockTakeRecordStatus?.toLowerCase(); | |||
| if (recordStatus === "pass" || recordStatus === "completed") { | |||
| return true; | |||
| } | |||
| return false; | |||
| }, []); | |||
| }, [selectedSession?.status]); | |||
| const uniqueWarehouses = Array.from( | |||
| new Set( | |||
| @@ -460,9 +467,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TableRow> | |||
| <TableCell>{t("Warehouse Location")}</TableCell> | |||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| <TableCell>{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -478,7 +486,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| </TableRow> | |||
| ) : ( | |||
| inventoryLotDetails.map((detail) => { | |||
| const isEditing = editingRecord?.id === detail.id; | |||
| const submitDisabled = isSubmitDisabled(detail); | |||
| const isFirstSubmit = | |||
| !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||
| @@ -513,19 +520,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| </Box> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| {/* Qty + Bad Qty 合并显示/输入 */} | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| <Stack spacing={1}> | |||
| {/* First */} | |||
| {isEditing && isFirstSubmit ? ( | |||
| {!submitDisabled && isFirstSubmit ? ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Typography variant="body2">{t("First")}:</Typography> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={firstQty} | |||
| onChange={(e) => setFirstQty(e.target.value)} | |||
| value={recordInputs[detail.id]?.firstQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -533,14 +545,23 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| "& .MuiInputBase-input::placeholder": { | |||
| color: "grey.400", // MUI light grey | |||
| opacity: 1, | |||
| }, | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={firstBadQty} | |||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||
| value={recordInputs[detail.id]?.firstBadQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -548,14 +569,18 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| height: "1.4375em", | |||
| padding: "4px 8px", | |||
| }, | |||
| "& .MuiInputBase-input::placeholder": { | |||
| color: "grey.400", // MUI light grey | |||
| opacity: 1, | |||
| }, | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| <Typography variant="body2"> | |||
| = | |||
| {formatNumber( | |||
| parseFloat(firstQty || "0") - | |||
| parseFloat(firstBadQty || "0") | |||
| parseFloat(recordInputs[detail.id]?.firstQty || "0") - | |||
| parseFloat(recordInputs[detail.id]?.firstBadQty || "0") | |||
| )} | |||
| </Typography> | |||
| </Stack> | |||
| @@ -576,14 +601,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| ) : null} | |||
| {/* Second */} | |||
| {isEditing && isSecondSubmit ? ( | |||
| {!submitDisabled && isSecondSubmit ? ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Typography variant="body2">{t("Second")}:</Typography> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={secondQty} | |||
| onChange={(e) => setSecondQty(e.target.value)} | |||
| value={recordInputs[detail.id]?.secondQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -597,8 +627,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={secondBadQty} | |||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||
| value={recordInputs[detail.id]?.secondBadQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -612,8 +647,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <Typography variant="body2"> | |||
| = | |||
| {formatNumber( | |||
| parseFloat(secondQty || "0") - | |||
| parseFloat(secondBadQty || "0") | |||
| parseFloat(recordInputs[detail.id]?.secondQty || "0") - | |||
| parseFloat(recordInputs[detail.id]?.secondBadQty || "0") | |||
| )} | |||
| </Typography> | |||
| </Stack> | |||
| @@ -635,7 +670,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| {!detail.firstStockTakeQty && | |||
| !detail.secondStockTakeQty && | |||
| !isEditing && ( | |||
| !submitDisabled && ( | |||
| <Typography | |||
| variant="body2" | |||
| color="text.secondary" | |||
| @@ -648,13 +683,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| {/* Remark */} | |||
| <TableCell sx={{ width: 180 }}> | |||
| {isEditing && isSecondSubmit ? ( | |||
| {!submitDisabled && isSecondSubmit ? ( | |||
| <> | |||
| <Typography variant="body2">{t("Remark")}</Typography> | |||
| <TextField | |||
| size="small" | |||
| value={remark} | |||
| onChange={(e) => setRemark(e.target.value)} | |||
| value={recordInputs[detail.id]?.remark || ""} | |||
| onChange={(e) => setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { | |||
| ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), | |||
| remark: e.target.value | |||
| } | |||
| }))} | |||
| sx={{ width: 150 }} | |||
| /> | |||
| </> | |||
| @@ -665,32 +706,38 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| )} | |||
| </TableCell> | |||
| <TableCell>{detail.uom || "-"}</TableCell> | |||
| <TableCell> | |||
| {detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="success" | |||
| /> | |||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="warning" | |||
| /> | |||
| ) : ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus || "")} | |||
| color="default" | |||
| /> | |||
| )} | |||
| </TableCell> | |||
| {detail.stockTakeRecordStatus === "completed" ? ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="success" | |||
| /> | |||
| ) : detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="default" | |||
| /> | |||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="warning" | |||
| /> | |||
| ) : ( | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus || "")} | |||
| color="default" | |||
| /> | |||
| )} | |||
| </TableCell> | |||
| <TableCell> | |||
| {isEditing ? ( | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| size="small" | |||
| @@ -700,24 +747,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| <Button size="small" onClick={handleCancelEdit}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| ) : ( | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| onClick={() => handleStartEdit(detail)} | |||
| disabled={submitDisabled} | |||
| > | |||
| {!detail.stockTakeRecordId | |||
| ? t("Input") | |||
| : detail.stockTakeRecordStatus === "notMatch" | |||
| ? t("Input") | |||
| : t("View")} | |||
| </Button> | |||
| )} | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| @@ -7,6 +7,8 @@ | |||
| "Qty": "盤點數量", | |||
| "UoM": "單位", | |||
| "mat": "物料", | |||
| "variance": "差異", | |||
| "Variance %": "差異百分比", | |||
| "fg": "成品", | |||
| "Back to List": "返回列表", | |||
| "Record Status": "記錄狀態", | |||