|
|
@@ -0,0 +1,808 @@ |
|
|
|
|
|
"use client"; |
|
|
|
|
|
|
|
|
|
|
|
import { |
|
|
|
|
|
Box, |
|
|
|
|
|
Button, |
|
|
|
|
|
Stack, |
|
|
|
|
|
Typography, |
|
|
|
|
|
Chip, |
|
|
|
|
|
CircularProgress, |
|
|
|
|
|
Table, |
|
|
|
|
|
TableBody, |
|
|
|
|
|
TableCell, |
|
|
|
|
|
TableContainer, |
|
|
|
|
|
TableHead, |
|
|
|
|
|
TableRow, |
|
|
|
|
|
Paper, |
|
|
|
|
|
TextField, |
|
|
|
|
|
Radio, |
|
|
|
|
|
TablePagination, |
|
|
|
|
|
} from "@mui/material"; |
|
|
|
|
|
import { useState, useCallback, useEffect, useMemo } from "react"; |
|
|
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
import { |
|
|
|
|
|
AllPickedStockTakeListReponse, |
|
|
|
|
|
InventoryLotDetailResponse, |
|
|
|
|
|
SaveApproverStockTakeRecordRequest, |
|
|
|
|
|
saveApproverStockTakeRecord, |
|
|
|
|
|
getApproverInventoryLotDetailsAll, |
|
|
|
|
|
BatchSaveApproverStockTakeAllRequest, |
|
|
|
|
|
batchSaveApproverStockTakeRecordsAll, |
|
|
|
|
|
updateStockTakeRecordStatusToNotMatch, |
|
|
|
|
|
} from "@/app/api/stockTake/actions"; |
|
|
|
|
|
import { useSession } from "next-auth/react"; |
|
|
|
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; |
|
|
|
|
|
|
|
|
|
|
|
interface ApproverStockTakeAllProps { |
|
|
|
|
|
selectedSession: AllPickedStockTakeListReponse; |
|
|
|
|
|
onBack: () => void; |
|
|
|
|
|
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type QtySelectionType = "first" | "second" | "approver"; |
|
|
|
|
|
|
|
|
|
|
|
const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
|
|
|
selectedSession, |
|
|
|
|
|
onBack, |
|
|
|
|
|
onSnackbar, |
|
|
|
|
|
}) => { |
|
|
|
|
|
const { t } = useTranslation(["inventory", "common"]); |
|
|
|
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
|
|
|
|
|
|
|
|
|
const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]); |
|
|
|
|
|
const [loadingDetails, setLoadingDetails] = useState(false); |
|
|
|
|
|
const [variancePercentTolerance, setVariancePercentTolerance] = useState<string>("5"); |
|
|
|
|
|
const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({}); |
|
|
|
|
|
const [approverQty, setApproverQty] = useState<Record<number, string>>({}); |
|
|
|
|
|
const [approverBadQty, setApproverBadQty] = useState<Record<number, string>>({}); |
|
|
|
|
|
const [saving, setSaving] = useState(false); |
|
|
|
|
|
const [batchSaving, setBatchSaving] = useState(false); |
|
|
|
|
|
const [updatingStatus, setUpdatingStatus] = useState(false); |
|
|
|
|
|
const [page, setPage] = useState(0); |
|
|
|
|
|
const [pageSize, setPageSize] = useState<number | string>("all"); |
|
|
|
|
|
const [total, setTotal] = useState(0); |
|
|
|
|
|
|
|
|
|
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
|
|
|
|
|
|
|
|
|
const handleChangePage = useCallback((_: unknown, newPage: number) => { |
|
|
|
|
|
setPage(newPage); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleChangeRowsPerPage = useCallback( |
|
|
|
|
|
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { |
|
|
|
|
|
const newSize = parseInt(event.target.value, 10); |
|
|
|
|
|
if (newSize === -1) { |
|
|
|
|
|
setPageSize("all"); |
|
|
|
|
|
} else if (!isNaN(newSize)) { |
|
|
|
|
|
setPageSize(newSize); |
|
|
|
|
|
} |
|
|
|
|
|
setPage(0); |
|
|
|
|
|
}, |
|
|
|
|
|
[] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const loadDetails = useCallback( |
|
|
|
|
|
async (pageNum: number, size: number | string) => { |
|
|
|
|
|
setLoadingDetails(true); |
|
|
|
|
|
try { |
|
|
|
|
|
let actualSize: number; |
|
|
|
|
|
if (size === "all") { |
|
|
|
|
|
if (total > 0) { |
|
|
|
|
|
actualSize = total; |
|
|
|
|
|
} else if (selectedSession.totalInventoryLotNumber > 0) { |
|
|
|
|
|
actualSize = selectedSession.totalInventoryLotNumber; |
|
|
|
|
|
} else { |
|
|
|
|
|
actualSize = 10000; |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
actualSize = typeof size === "string" ? parseInt(size, 10) : size; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const response = await getApproverInventoryLotDetailsAll( |
|
|
|
|
|
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, |
|
|
|
|
|
pageNum, |
|
|
|
|
|
actualSize |
|
|
|
|
|
); |
|
|
|
|
|
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); |
|
|
|
|
|
setTotal(response.total || 0); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error(e); |
|
|
|
|
|
setInventoryLotDetails([]); |
|
|
|
|
|
setTotal(0); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setLoadingDetails(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[selectedSession, total] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
loadDetails(page, pageSize); |
|
|
|
|
|
}, [page, pageSize, loadDetails]); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const newSelections: Record<number, QtySelectionType> = {}; |
|
|
|
|
|
inventoryLotDetails.forEach((detail) => { |
|
|
|
|
|
if (!qtySelection[detail.id]) { |
|
|
|
|
|
if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) { |
|
|
|
|
|
newSelections[detail.id] = "second"; |
|
|
|
|
|
} else { |
|
|
|
|
|
newSelections[detail.id] = "first"; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(newSelections).length > 0) { |
|
|
|
|
|
setQtySelection((prev) => ({ ...prev, ...newSelections })); |
|
|
|
|
|
} |
|
|
|
|
|
}, [inventoryLotDetails, qtySelection]); |
|
|
|
|
|
|
|
|
|
|
|
const calculateDifference = useCallback( |
|
|
|
|
|
(detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { |
|
|
|
|
|
let selectedQty = 0; |
|
|
|
|
|
|
|
|
|
|
|
if (selection === "first") { |
|
|
|
|
|
selectedQty = detail.firstStockTakeQty || 0; |
|
|
|
|
|
} else if (selection === "second") { |
|
|
|
|
|
selectedQty = detail.secondStockTakeQty || 0; |
|
|
|
|
|
} else if (selection === "approver") { |
|
|
|
|
|
selectedQty = |
|
|
|
|
|
(parseFloat(approverQty[detail.id] || "0") - |
|
|
|
|
|
parseFloat(approverBadQty[detail.id] || "0")) || 0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const bookQty = detail.bookQty != null ? detail.bookQty : detail.availableQty || 0; |
|
|
|
|
|
return selectedQty - bookQty; |
|
|
|
|
|
}, |
|
|
|
|
|
[approverQty, approverBadQty] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const filteredDetails = useMemo(() => { |
|
|
|
|
|
const percent = parseFloat(variancePercentTolerance || "0"); |
|
|
|
|
|
const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent; |
|
|
|
|
|
|
|
|
|
|
|
return inventoryLotDetails.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; |
|
|
|
|
|
}); |
|
|
|
|
|
}, [ |
|
|
|
|
|
inventoryLotDetails, |
|
|
|
|
|
variancePercentTolerance, |
|
|
|
|
|
qtySelection, |
|
|
|
|
|
calculateDifference, |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
const handleSaveApproverStockTake = useCallback( |
|
|
|
|
|
async (detail: InventoryLotDetailResponse) => { |
|
|
|
|
|
if (!selectedSession || !currentUserId) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const selection = qtySelection[detail.id] || "first"; |
|
|
|
|
|
let finalQty: number; |
|
|
|
|
|
let finalBadQty: number; |
|
|
|
|
|
|
|
|
|
|
|
if (selection === "first") { |
|
|
|
|
|
if (detail.firstStockTakeQty == null) { |
|
|
|
|
|
onSnackbar(t("First QTY is not available"), "error"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
finalQty = detail.firstStockTakeQty; |
|
|
|
|
|
finalBadQty = detail.firstBadQty || 0; |
|
|
|
|
|
} else if (selection === "second") { |
|
|
|
|
|
if (detail.secondStockTakeQty == null) { |
|
|
|
|
|
onSnackbar(t("Second QTY is not available"), "error"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
finalQty = detail.secondStockTakeQty; |
|
|
|
|
|
finalBadQty = detail.secondBadQty || 0; |
|
|
|
|
|
} else { |
|
|
|
|
|
const approverQtyValue = approverQty[detail.id]; |
|
|
|
|
|
const approverBadQtyValue = approverBadQty[detail.id]; |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
approverQtyValue === undefined || |
|
|
|
|
|
approverQtyValue === null || |
|
|
|
|
|
approverQtyValue === "" |
|
|
|
|
|
) { |
|
|
|
|
|
onSnackbar(t("Please enter Approver QTY"), "error"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if ( |
|
|
|
|
|
approverBadQtyValue === undefined || |
|
|
|
|
|
approverBadQtyValue === null || |
|
|
|
|
|
approverBadQtyValue === "" |
|
|
|
|
|
) { |
|
|
|
|
|
onSnackbar(t("Please enter Approver Bad QTY"), "error"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
finalQty = parseFloat(approverQtyValue) || 0; |
|
|
|
|
|
finalBadQty = parseFloat(approverBadQtyValue) || 0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setSaving(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const request: SaveApproverStockTakeRecordRequest = { |
|
|
|
|
|
stockTakeRecordId: detail.stockTakeRecordId || null, |
|
|
|
|
|
qty: finalQty, |
|
|
|
|
|
badQty: finalBadQty, |
|
|
|
|
|
approverId: currentUserId, |
|
|
|
|
|
approverQty: selection === "approver" ? finalQty : null, |
|
|
|
|
|
approverBadQty: selection === "approver" ? finalBadQty : null, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
await saveApproverStockTakeRecord(request, selectedSession.stockTakeId); |
|
|
|
|
|
|
|
|
|
|
|
onSnackbar(t("Approver stock take record saved successfully"), "success"); |
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
|
|
|
if (e?.message) { |
|
|
|
|
|
errorMessage = e.message; |
|
|
|
|
|
} else if (e?.response) { |
|
|
|
|
|
try { |
|
|
|
|
|
const errorData = await e.response.json(); |
|
|
|
|
|
errorMessage = errorData.message || errorData.error || errorMessage; |
|
|
|
|
|
} catch { |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onSnackbar(errorMessage, "error"); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setSaving(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[selectedSession, currentUserId, qtySelection, approverQty, approverBadQty, t, onSnackbar] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleUpdateStatusToNotMatch = useCallback( |
|
|
|
|
|
async (detail: InventoryLotDetailResponse) => { |
|
|
|
|
|
if (!detail.stockTakeRecordId) { |
|
|
|
|
|
onSnackbar(t("Stock take record ID is required"), "error"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setUpdatingStatus(true); |
|
|
|
|
|
try { |
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
|
|
|
if (e?.message) { |
|
|
|
|
|
errorMessage = e.message; |
|
|
|
|
|
} else if (e?.response) { |
|
|
|
|
|
try { |
|
|
|
|
|
const errorData = await e.response.json(); |
|
|
|
|
|
errorMessage = errorData.message || errorData.error || errorMessage; |
|
|
|
|
|
} catch { |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onSnackbar(errorMessage, "error"); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setUpdatingStatus(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[t, onSnackbar] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handleBatchSubmitAll = useCallback(async () => { |
|
|
|
|
|
if (!selectedSession || !currentUserId) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setBatchSaving(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const request: BatchSaveApproverStockTakeAllRequest = { |
|
|
|
|
|
stockTakeId: selectedSession.stockTakeId, |
|
|
|
|
|
approverId: currentUserId, |
|
|
|
|
|
variancePercentTolerance: parseFloat(variancePercentTolerance || "0") || undefined, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const result = await batchSaveApproverStockTakeRecordsAll(request); |
|
|
|
|
|
|
|
|
|
|
|
onSnackbar( |
|
|
|
|
|
t("Batch approver save completed: {{success}} success, {{errors}} errors", { |
|
|
|
|
|
success: result.successCount, |
|
|
|
|
|
errors: result.errorCount, |
|
|
|
|
|
}), |
|
|
|
|
|
result.errorCount > 0 ? "warning" : "success" |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
await loadDetails(page, pageSize); |
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
console.error("handleBatchSubmitAll (all): Error:", e); |
|
|
|
|
|
let errorMessage = t("Failed to batch save approver stock take records"); |
|
|
|
|
|
|
|
|
|
|
|
if (e?.message) { |
|
|
|
|
|
errorMessage = e.message; |
|
|
|
|
|
} else if (e?.response) { |
|
|
|
|
|
try { |
|
|
|
|
|
const errorData = await e.response.json(); |
|
|
|
|
|
errorMessage = errorData.message || errorData.error || errorMessage; |
|
|
|
|
|
} catch { |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onSnackbar(errorMessage, "error"); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setBatchSaving(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize]); |
|
|
|
|
|
|
|
|
|
|
|
const formatNumber = (num: number | null | undefined): string => { |
|
|
|
|
|
if (num == null) return "0"; |
|
|
|
|
|
return num.toLocaleString("en-US", { |
|
|
|
|
|
minimumFractionDigits: 0, |
|
|
|
|
|
maximumFractionDigits: 0, |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const uniqueWarehouses = useMemo( |
|
|
|
|
|
() => |
|
|
|
|
|
Array.from( |
|
|
|
|
|
new Set( |
|
|
|
|
|
inventoryLotDetails |
|
|
|
|
|
.map((detail) => detail.warehouse) |
|
|
|
|
|
.filter((warehouse) => warehouse && warehouse.trim() !== "") |
|
|
|
|
|
) |
|
|
|
|
|
).join(", "), |
|
|
|
|
|
[inventoryLotDetails] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<Box> |
|
|
|
|
|
<Button |
|
|
|
|
|
onClick={onBack} |
|
|
|
|
|
sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Back to List")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
justifyContent="space-between" |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
sx={{ mb: 2 }} |
|
|
|
|
|
> |
|
|
|
|
|
<Typography variant="h6" sx={{ mb: 2 }}> |
|
|
|
|
|
|
|
|
|
|
|
{uniqueWarehouses && ( |
|
|
|
|
|
<> {t("Warehouse")}: {uniqueWarehouses}</> |
|
|
|
|
|
)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
<Stack direction="row" spacing={2} alignItems="center"> |
|
|
|
|
|
<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 }} |
|
|
|
|
|
/> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
color="primary" |
|
|
|
|
|
onClick={handleBatchSubmitAll} |
|
|
|
|
|
disabled={batchSaving} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Batch Save All")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
{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")} |
|
|
|
|
|
/> |
|
|
|
|
|
<TableContainer component={Paper}> |
|
|
|
|
|
<Table> |
|
|
|
|
|
<TableHead> |
|
|
|
|
|
<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("Record Status")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Action")}</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
</TableHead> |
|
|
|
|
|
<TableBody> |
|
|
|
|
|
{filteredDetails.length === 0 ? ( |
|
|
|
|
|
<TableRow> |
|
|
|
|
|
<TableCell colSpan={7} align="center"> |
|
|
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
|
|
{t("No data")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
) : ( |
|
|
|
|
|
filteredDetails.map((detail) => { |
|
|
|
|
|
const hasFirst = |
|
|
|
|
|
detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0; |
|
|
|
|
|
const hasSecond = |
|
|
|
|
|
detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; |
|
|
|
|
|
const selection = |
|
|
|
|
|
qtySelection[detail.id] || (hasSecond ? "second" : "first"); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<TableRow key={detail.id}> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{detail.warehouseArea || "-"} |
|
|
|
|
|
{detail.warehouseSlot || "-"} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell |
|
|
|
|
|
sx={{ |
|
|
|
|
|
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>{detail.uom || "-"}</TableCell> |
|
|
|
|
|
<TableCell sx={{ minWidth: 300 }}> |
|
|
|
|
|
{detail.finalQty != null ? ( |
|
|
|
|
|
<Stack spacing={0.5}> |
|
|
|
|
|
{(() => { |
|
|
|
|
|
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 && ( |
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
spacing={1} |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
> |
|
|
|
|
|
<Radio |
|
|
|
|
|
size="small" |
|
|
|
|
|
checked={selection === "first"} |
|
|
|
|
|
onChange={() => |
|
|
|
|
|
setQtySelection({ |
|
|
|
|
|
...qtySelection, |
|
|
|
|
|
[detail.id]: "first", |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
/> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("First")}:{" "} |
|
|
|
|
|
{formatNumber( |
|
|
|
|
|
(detail.firstStockTakeQty ?? 0) + |
|
|
|
|
|
(detail.firstBadQty ?? 0) |
|
|
|
|
|
)}{" "} |
|
|
|
|
|
({detail.firstBadQty ?? 0}) ={" "} |
|
|
|
|
|
{formatNumber(detail.firstStockTakeQty ?? 0)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{hasSecond && ( |
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
spacing={1} |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
> |
|
|
|
|
|
<Radio |
|
|
|
|
|
size="small" |
|
|
|
|
|
checked={selection === "second"} |
|
|
|
|
|
onChange={() => |
|
|
|
|
|
setQtySelection({ |
|
|
|
|
|
...qtySelection, |
|
|
|
|
|
[detail.id]: "second", |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
/> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("Second")}:{" "} |
|
|
|
|
|
{formatNumber( |
|
|
|
|
|
(detail.secondStockTakeQty ?? 0) + |
|
|
|
|
|
(detail.secondBadQty ?? 0) |
|
|
|
|
|
)}{" "} |
|
|
|
|
|
({detail.secondBadQty ?? 0}) ={" "} |
|
|
|
|
|
{formatNumber(detail.secondStockTakeQty ?? 0)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{hasSecond && ( |
|
|
|
|
|
<Stack |
|
|
|
|
|
direction="row" |
|
|
|
|
|
spacing={1} |
|
|
|
|
|
alignItems="center" |
|
|
|
|
|
> |
|
|
|
|
|
<Radio |
|
|
|
|
|
size="small" |
|
|
|
|
|
checked={selection === "approver"} |
|
|
|
|
|
onChange={() => |
|
|
|
|
|
setQtySelection({ |
|
|
|
|
|
...qtySelection, |
|
|
|
|
|
[detail.id]: "approver", |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
/> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("Approver Input")}: |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={approverQty[detail.id] || ""} |
|
|
|
|
|
onChange={(e) => |
|
|
|
|
|
setApproverQty({ |
|
|
|
|
|
...approverQty, |
|
|
|
|
|
[detail.id]: e.target.value, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 130, |
|
|
|
|
|
minWidth: 130, |
|
|
|
|
|
"& .MuiInputBase-input": { |
|
|
|
|
|
height: "1.4375em", |
|
|
|
|
|
padding: "4px 8px", |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
placeholder={t("Stock Take Qty")} |
|
|
|
|
|
disabled={selection !== "approver"} |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={approverBadQty[detail.id] || ""} |
|
|
|
|
|
onChange={(e) => |
|
|
|
|
|
setApproverBadQty({ |
|
|
|
|
|
...approverBadQty, |
|
|
|
|
|
[detail.id]: e.target.value, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 130, |
|
|
|
|
|
minWidth: 130, |
|
|
|
|
|
"& .MuiInputBase-input": { |
|
|
|
|
|
height: "1.4375em", |
|
|
|
|
|
padding: "4px 8px", |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
placeholder={t("Bad Qty")} |
|
|
|
|
|
disabled={selection !== "approver"} |
|
|
|
|
|
/> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
={" "} |
|
|
|
|
|
{formatNumber( |
|
|
|
|
|
parseFloat(approverQty[detail.id] || "0") - |
|
|
|
|
|
parseFloat( |
|
|
|
|
|
approverBadQty[detail.id] || "0" |
|
|
|
|
|
) |
|
|
|
|
|
)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{(() => { |
|
|
|
|
|
let selectedQty = 0; |
|
|
|
|
|
|
|
|
|
|
|
if (selection === "first") { |
|
|
|
|
|
selectedQty = detail.firstStockTakeQty || 0; |
|
|
|
|
|
} else if (selection === "second") { |
|
|
|
|
|
selectedQty = detail.secondStockTakeQty || 0; |
|
|
|
|
|
} else if (selection === "approver") { |
|
|
|
|
|
selectedQty = |
|
|
|
|
|
(parseFloat(approverQty[detail.id] || "0") - |
|
|
|
|
|
parseFloat( |
|
|
|
|
|
approverBadQty[detail.id] || "0" |
|
|
|
|
|
)) || 0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const bookQty = |
|
|
|
|
|
detail.bookQty != null |
|
|
|
|
|
? detail.bookQty |
|
|
|
|
|
: detail.availableQty || 0; |
|
|
|
|
|
const difference = selectedQty - bookQty; |
|
|
|
|
|
const differenceColor = |
|
|
|
|
|
detail.stockTakeRecordStatus === "completed" |
|
|
|
|
|
? "text.secondary" |
|
|
|
|
|
: difference !== 0 |
|
|
|
|
|
? "error.main" |
|
|
|
|
|
: "success.main"; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ fontWeight: "bold", color: differenceColor }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Difference")}:{" "} |
|
|
|
|
|
{t("selected stock take qty")}( |
|
|
|
|
|
{formatNumber(selectedQty)}) -{" "} |
|
|
|
|
|
{t("book qty")}( |
|
|
|
|
|
{formatNumber(bookQty)}) ={" "} |
|
|
|
|
|
{formatNumber(difference)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
); |
|
|
|
|
|
})()} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
|
|
|
|
|
|
<TableCell> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{detail.remarks || "-"} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
{detail.stockTakeRecordId && |
|
|
|
|
|
detail.stockTakeRecordStatus !== "notMatch" && ( |
|
|
|
|
|
<Box> |
|
|
|
|
|
<Button |
|
|
|
|
|
size="small" |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
color="warning" |
|
|
|
|
|
onClick={() => |
|
|
|
|
|
handleUpdateStatusToNotMatch(detail) |
|
|
|
|
|
} |
|
|
|
|
|
disabled={ |
|
|
|
|
|
updatingStatus || |
|
|
|
|
|
detail.stockTakeRecordStatus === "completed" |
|
|
|
|
|
} |
|
|
|
|
|
> |
|
|
|
|
|
{t("ReStockTake")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
)} |
|
|
|
|
|
<br /> |
|
|
|
|
|
{detail.finalQty == null && ( |
|
|
|
|
|
<Box> |
|
|
|
|
|
<Button |
|
|
|
|
|
size="small" |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
onClick={() => handleSaveApproverStockTake(detail)} |
|
|
|
|
|
disabled={saving} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Save")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
); |
|
|
|
|
|
}) |
|
|
|
|
|
)} |
|
|
|
|
|
</TableBody> |
|
|
|
|
|
</Table> |
|
|
|
|
|
</TableContainer> |
|
|
|
|
|
<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")} |
|
|
|
|
|
/> |
|
|
|
|
|
</> |
|
|
|
|
|
)} |
|
|
|
|
|
</Box> |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export default ApproverStockTakeAll; |
|
|
|
|
|
|