| @@ -141,6 +141,14 @@ export default function ReportPage() { | |||
| } | |||
| // Clear dynamic options when report changes | |||
| setDynamicOptions({}); | |||
| // Default "All" (no filter) for stock take variance report conditions. | |||
| if (selectedReportId === 'rep-012') { | |||
| setCriteria({ | |||
| store_id: 'All', | |||
| status: 'All', | |||
| }); | |||
| } | |||
| }, [selectedReportId]); | |||
| // React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice. | |||
| @@ -29,6 +29,7 @@ export interface InventoryLotDetailResponse { | |||
| warehouseSlot: string; | |||
| warehouseArea: string; | |||
| warehouse: string; | |||
| storeId?: string | null; | |||
| varianceQty: number | null; | |||
| status: string; | |||
| remarks: string | null; | |||
| @@ -137,6 +138,8 @@ export interface AllPickedStockTakeListReponse { | |||
| endTime: string | null; | |||
| planStartDate: string | null; | |||
| stockTakeSectionDescription: string | null; | |||
| warehouseArea: string | null; | |||
| storeId: string | null; | |||
| reStockTakeTrueFalse: boolean; | |||
| } | |||
| @@ -254,7 +257,7 @@ export const getStockTakeRecords = async () => { | |||
| export const getStockTakeRecordsPaged = async ( | |||
| pageNum: number, | |||
| pageSize: number, | |||
| params?: { sectionDescription?: string; stockTakeSections?: string } | |||
| params?: { sectionDescription?: string; stockTakeSections?: string; status?: string; area?: string; storeId?: string } | |||
| ) => { | |||
| const searchParams = new URLSearchParams(); | |||
| searchParams.set("pageNum", String(pageNum)); | |||
| @@ -265,6 +268,15 @@ export const getStockTakeRecordsPaged = async ( | |||
| if (params?.stockTakeSections?.trim()) { | |||
| searchParams.set("stockTakeSections", params.stockTakeSections.trim()); | |||
| } | |||
| if (params?.status && params.status !== "All") { | |||
| searchParams.set("status", params.status); | |||
| } | |||
| if (params?.area?.trim()) { | |||
| searchParams.set("area", params.area.trim()); | |||
| } | |||
| if (params?.storeId && params.storeId !== "All") { | |||
| searchParams.set("storeId", params.storeId); | |||
| } | |||
| const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`; | |||
| const res = await serverFetchJson<RecordsRes<AllPickedStockTakeListReponse>>(url, { method: "GET" }); | |||
| return res; | |||
| @@ -39,5 +39,6 @@ export interface StockTakeSectionInfo { | |||
| id: string; | |||
| stockTakeSection: string; | |||
| stockTakeSectionDescription: string | null; | |||
| storeId?: string | null; | |||
| warehouseCount: number; | |||
| } | |||
| @@ -44,9 +44,6 @@ import { | |||
| checkAndCompletePickOrderByConsoCode, | |||
| confirmLotSubstitution, | |||
| updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加 | |||
| batchSubmitList, // ✅ 添加 | |||
| batchSubmitListRequest, // ✅ 添加 | |||
| batchSubmitListLineRequest, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| @@ -3783,92 +3780,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| }); | |||
| } | |||
| // Hold-only:與整批相同規則,只送一筆 batch,把實揈/完成狀態寫回 DB | |||
| if (useHoldOnlyApi && pickOrderIdEarly && solId > 0) { | |||
| const freshData = | |||
| await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderIdEarly); | |||
| const flatLots = getAllLotsFromHierarchical(freshData); | |||
| const lotRow = flatLots.find( | |||
| (l: any) => Number(l.stockOutLineId) === solId, | |||
| ); | |||
| if (!lotRow) { | |||
| throw new Error( | |||
| "Could not find lot row after refresh for batch submit", | |||
| ); | |||
| } | |||
| const requiredQty = Number( | |||
| lotRow.requiredQty || lotRow.pickOrderLineRequiredQty || 0, | |||
| ); | |||
| const issuePickedVal = picked; | |||
| const currentActualPickQty = Number( | |||
| issuePickedVal ?? lotRow.actualPickQty ?? 0, | |||
| ); | |||
| const onlyComplete = | |||
| lotRow.stockOutLineStatus === "partially_completed" || | |||
| lotRow.stockOutLineStatus === "PARTIALLY_COMPLETE" || | |||
| issuePickedVal !== undefined; | |||
| const expired = isLotAvailabilityExpired(lotRow); | |||
| const unavailable = isInventoryLotLineUnavailable(lotRow); | |||
| let targetActual: number; | |||
| let newStatus: string; | |||
| if (unavailable) { | |||
| targetActual = currentActualPickQty; | |||
| newStatus = "completed"; | |||
| } else if (expired && issuePickedVal === undefined) { | |||
| targetActual = 0; | |||
| newStatus = "completed"; | |||
| } else if (onlyComplete) { | |||
| targetActual = currentActualPickQty; | |||
| newStatus = "completed"; | |||
| } else { | |||
| const remainingQty = Math.max( | |||
| 0, | |||
| requiredQty - currentActualPickQty, | |||
| ); | |||
| const cumulativeQty = currentActualPickQty + remainingQty; | |||
| targetActual = cumulativeQty; | |||
| newStatus = "partially_completed"; | |||
| if (requiredQty > 0 && cumulativeQty >= requiredQty) { | |||
| newStatus = "completed"; | |||
| } | |||
| } | |||
| const line: batchSubmitListLineRequest = { | |||
| stockOutLineId: solId, | |||
| pickOrderLineId: Number(lotRow.pickOrderLineId), | |||
| inventoryLotLineId: lotRow.lotId ? Number(lotRow.lotId) : null, | |||
| requiredQty, | |||
| actualPickQty: targetActual, | |||
| stockOutLineStatus: newStatus, | |||
| pickOrderConsoCode: String(lotRow.pickOrderConsoCode || ""), | |||
| noLot: Boolean(lotRow.noLot === true), | |||
| }; | |||
| const batchResult = await batchSubmitList({ | |||
| userId: currentUserId || 0, | |||
| lines: [line], | |||
| }); | |||
| if (!batchResult || batchResult.code !== "SUCCESS") { | |||
| throw new Error( | |||
| batchResult?.message || | |||
| "Batch submit failed after hold adjustment", | |||
| ); | |||
| } | |||
| const conso = String(lotRow.pickOrderConsoCode || "").trim(); | |||
| if (conso) { | |||
| try { | |||
| await checkAndCompletePickOrderByConsoCode(conso); | |||
| } catch (e) { | |||
| console.error("❌ completion check after single batch:", e); | |||
| } | |||
| } | |||
| } | |||
| setPickExecutionFormOpen(false); | |||
| setSelectedLotForExecutionForm(null); | |||
| @@ -3880,16 +3791,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| }, | |||
| [ | |||
| fetchJobOrderData, | |||
| getAllLotsFromHierarchical, | |||
| currentUserId, | |||
| selectedLotForExecutionForm, | |||
| updateHandledBy, | |||
| filterArgs, | |||
| session?.user?.name, | |||
| batchSubmitList, | |||
| checkAndCompletePickOrderByConsoCode, | |||
| isLotAvailabilityExpired, | |||
| isInventoryLotLineUnavailable, | |||
| ], | |||
| ); | |||
| // Calculate remaining required quantity | |||
| @@ -689,7 +689,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| variant="outlined" | |||
| color="warning" | |||
| onClick={() => handleUpdateStatusToNotMatch(detail)} | |||
| disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"} | |||
| disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"||hasSecond} | |||
| > | |||
| {t("ReStockTake")} | |||
| </Button> | |||
| @@ -18,6 +18,15 @@ import { | |||
| Radio, | |||
| TablePagination, | |||
| TableSortLabel, | |||
| Card, | |||
| CardContent, | |||
| CardActions, | |||
| Grid, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| Autocomplete, | |||
| } from "@mui/material"; | |||
| import { useState, useCallback, useEffect, useMemo } from "react"; | |||
| import { Collapse } from "@mui/material"; | |||
| @@ -37,7 +46,6 @@ import { | |||
| updateStockTakeRecordStatusToNotMatch, | |||
| type ApproverInventoryLotDetailsQuery, | |||
| } from "@/app/api/stockTake/actions"; | |||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | |||
| import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| @@ -66,13 +74,12 @@ type ApprovedSortKey = | |||
| | "stockTakerName" | |||
| | "variance"; | |||
| type ApproverSearchKey = "sectionDescription" | "stockTakeSession" | "itemKeyword" | "warehouseKeyword"|"status"; | |||
| type ApproverSearchFilters = { | |||
| sectionDescription: string; | |||
| stockTakeSession: string; | |||
| itemKeyword: string; | |||
| warehouseKeyword: string; | |||
| storeId: string; | |||
| status: string; | |||
| }; | |||
| @@ -85,15 +92,6 @@ function buildApproverInventoryQuery(filters: ApproverSearchFilters): ApproverIn | |||
| }; | |||
| } | |||
| function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean { | |||
| return ( | |||
| (f.sectionDescription && f.sectionDescription !== "All") || | |||
| f.stockTakeSession.trim() !== "" || | |||
| f.itemKeyword.trim() !== "" || | |||
| f.warehouseKeyword.trim() !== "" | |||
| ); | |||
| } | |||
| function isBlankApproverField(value: string | undefined): boolean { | |||
| return value == null || String(value).trim() === ""; | |||
| } | |||
| @@ -202,9 +200,15 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| const [total, setTotal] = useState(0); | |||
| const [approvedSortKey, setApprovedSortKey] = useState<ApprovedSortKey | null>(null); | |||
| const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); | |||
| const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState< | |||
| { value: string; label: string }[] | |||
| >([]); | |||
| const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); | |||
| const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]); | |||
| const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All"); | |||
| const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>(""); | |||
| const [searchItemKeyword, setSearchItemKeyword] = useState<string>(""); | |||
| const [searchWarehouseKeyword, setSearchWarehouseKeyword] = useState<string>(""); | |||
| const [searchStoreId, setSearchStoreId] = useState<string>("All"); | |||
| const [searchStatus, setSearchStatus] = useState<string>(mode === "pending" ? "pass" : "All"); | |||
| const [showFilters, setShowFilters] = useState(true) | |||
| const [appliedFilters, setAppliedFilters] = useState<ApproverSearchFilters | null>(null); | |||
| @@ -227,76 +231,32 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| [] | |||
| ); | |||
| const handleApproverSearchBoxSearch = useCallback( | |||
| (inputs: Record<ApproverSearchKey | `${ApproverSearchKey}To`, string>) => { | |||
| const next: ApproverSearchFilters = { | |||
| sectionDescription: inputs.sectionDescription || "All", | |||
| stockTakeSession: inputs.stockTakeSession || "", | |||
| itemKeyword: inputs.itemKeyword || "", | |||
| warehouseKeyword: inputs.warehouseKeyword || "", | |||
| status: inputs.status || "All", | |||
| }; | |||
| /* | |||
| if (!hasAnyApproverSearchCriterion(next)) { | |||
| onSnackbar(t("Please set at least one search criterion"), "warning"); | |||
| return; | |||
| } | |||
| */ | |||
| setAppliedFilters(next); | |||
| setPage(0); | |||
| }, | |||
| [onSnackbar, t] | |||
| ); | |||
| const handleApproverSearchBoxReset = useCallback(() => { | |||
| const handleSearch = useCallback(() => { | |||
| const next: ApproverSearchFilters = { | |||
| sectionDescription: searchSectionDescription || "All", | |||
| stockTakeSession: searchStockTakeSession || "", | |||
| itemKeyword: searchItemKeyword || "", | |||
| warehouseKeyword: searchWarehouseKeyword || "", | |||
| storeId: searchStoreId || "All", | |||
| status: mode === "pending" ? (searchStatus || "pass") : "All", | |||
| }; | |||
| setAppliedFilters(next); | |||
| setPage(0); | |||
| }, [searchSectionDescription, searchStockTakeSession, searchItemKeyword, searchWarehouseKeyword, searchStoreId, searchStatus, mode]); | |||
| const handleResetSearch = useCallback(() => { | |||
| const defaultStatus = mode === "pending" ? "pass" : "All"; | |||
| setSearchSectionDescription("All"); | |||
| setSearchStockTakeSession(""); | |||
| setSearchItemKeyword(""); | |||
| setSearchWarehouseKeyword(""); | |||
| setSearchStoreId("All"); | |||
| setSearchStatus(defaultStatus); | |||
| setAppliedFilters(null); | |||
| setPage(0); | |||
| setInventoryLotDetails([]); | |||
| setTotal(0); | |||
| }, []); | |||
| const approverSearchCriteria: Criterion<ApproverSearchKey>[] = useMemo( | |||
| () => [ | |||
| { | |||
| type: "autocomplete", | |||
| label: t("Stock Take Section Description"), | |||
| paramName: "sectionDescription", | |||
| options: sectionDescriptionAutocompleteOptions, | |||
| needAll: true, | |||
| }, | |||
| { | |||
| type: "text", | |||
| label: t("Stock Take Section (can use , to search multiple sections)"), | |||
| paramName: "stockTakeSession", | |||
| placeholder: "", | |||
| }, | |||
| { | |||
| type: "text", | |||
| label: t("Item"), | |||
| paramName: "itemKeyword", | |||
| placeholder: "", | |||
| }, | |||
| { | |||
| type: "text", | |||
| label: t("Warehouse"), | |||
| paramName: "warehouseKeyword", | |||
| placeholder: "", | |||
| }, | |||
| { | |||
| type: "select-labelled", | |||
| label: t("Record Status"), | |||
| paramName: "status", | |||
| options: [ | |||
| { label: t("All"), value: "All" }, | |||
| { label: t("Pending"), value: "pending" }, | |||
| { label: t("Not Match"), value: "notMatch" }, | |||
| { label: t("Pass"), value: "pass" }, // UI=Pass,值=completed | |||
| ], | |||
| } | |||
| ], | |||
| [t, sectionDescriptionAutocompleteOptions] | |||
| ); | |||
| }, [mode]); | |||
| const loadDetails = useCallback(async (filters: ApproverSearchFilters) => { | |||
| setLoadingDetails(true); | |||
| @@ -342,13 +302,19 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| fetchStockTakeSections() | |||
| .then((sections) => { | |||
| const descSet = new Set<string>(); | |||
| const sectionSet = new Set<string>(); | |||
| const storeSet = new Set<string>(["2F", "4F"]); | |||
| sections.forEach((s) => { | |||
| const section = s.stockTakeSection?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| const desc = s.stockTakeSectionDescription?.trim(); | |||
| if (desc) descSet.add(desc); | |||
| const storeId = s.storeId?.trim(); | |||
| if (storeId) storeSet.add(storeId); | |||
| }); | |||
| setSectionDescriptionAutocompleteOptions( | |||
| Array.from(descSet).map((desc) => ({ value: desc, label: desc })) | |||
| ); | |||
| setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); | |||
| setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); | |||
| setStoreIdOptions(Array.from(storeSet).sort((a, b) => a.localeCompare(b))); | |||
| }) | |||
| .catch((e) => { | |||
| console.error("Failed to load section descriptions for approver search:", e); | |||
| @@ -361,6 +327,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| setApproverQty({}); | |||
| setApproverBadQty({}); | |||
| setAppliedFilters(null); | |||
| setSearchStatus(mode === "pending" ? "pass" : "All"); | |||
| setPage(0); | |||
| setInventoryLotDetails([]); | |||
| setTotal(0); | |||
| @@ -491,8 +458,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| const percent = parseFloat(variancePercentTolerance || "0"); | |||
| const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent; | |||
| const statusFilter = appliedFilters?.status ?? "All"; | |||
| const statusFilter = mode === "pending" ? (appliedFilters?.status ?? "pass") : "All"; | |||
| const storeIdFilter = appliedFilters?.storeId ?? "All"; | |||
| return inventoryLotDetails.filter((detail) => { | |||
| if (storeIdFilter !== "All") { | |||
| if ((detail.storeId || "").trim().toLowerCase() !== storeIdFilter.trim().toLowerCase()) { | |||
| return false; | |||
| } | |||
| } | |||
| if (statusFilter !== "All") { | |||
| const rowStatus = normalizeStatus(detail.stockTakeRecordStatus); | |||
| const wanted = normalizeStatus(statusFilter); | |||
| @@ -526,6 +499,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| variancePercentTolerance, | |||
| qtySelection, | |||
| calculateDifference, | |||
| appliedFilters, | |||
| mode, | |||
| ]); | |||
| const sortedDetails = useMemo(() => { | |||
| @@ -890,7 +865,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| { | |||
| field: "qtyBlock", | |||
| headerName: t("Stock Take Qty(include Bad Qty)= Available Qty"), | |||
| minWidth: 420, | |||
| minWidth: 320, | |||
| flex: 3, | |||
| sortable: false, | |||
| renderCell: (params: GridRenderCellParams<InventoryLotDetailResponse>) => { | |||
| @@ -949,7 +924,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| {formatNumber( | |||
| (detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0) | |||
| )}{" "} | |||
| ({detail.firstBadQty ?? 0}) ={" "} | |||
| {/* ({detail.firstBadQty ?? 0}) */} | |||
| ={" "} | |||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||
| </Typography> | |||
| </Stack> | |||
| @@ -974,7 +950,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| (detail.secondStockTakeQty ?? 0) + | |||
| (detail.secondBadQty ?? 0) | |||
| )}{" "} | |||
| ({detail.secondBadQty ?? 0}) ={" "} | |||
| {/* ({detail.secondBadQty ?? 0}) */} | |||
| ={" "} | |||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||
| </Typography> | |||
| </Stack> | |||
| @@ -1013,6 +990,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| /> | |||
| {/* | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| @@ -1030,6 +1008,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| disabled={mode === "approved" || selection !== "approver"} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| /> | |||
| */ | |||
| } | |||
| <Typography variant="body2" sx={{ minWidth: 90 }}> | |||
| = {formatNumber(approverGoodQty)} | |||
| </Typography> | |||
| @@ -1124,8 +1104,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| sortable: false, | |||
| renderCell: (params) => { | |||
| const detail = params.row; | |||
| const hasSecond = | |||
| detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; | |||
| const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; | |||
| const selection = | |||
| qtySelection[detail.id] || (hasSecond ? "second" : "first"); | |||
| @@ -1149,7 +1128,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| variant="outlined" | |||
| color="warning" | |||
| onClick={() => handleUpdateStatusToNotMatch(detail)} | |||
| disabled={updatingStatus} | |||
| disabled={updatingStatus||hasSecond} | |||
| > | |||
| {t("Not Match")} | |||
| </Button> | |||
| @@ -1189,6 +1168,25 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| ]); | |||
| const effectivePageSize = | |||
| pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number); | |||
| useEffect(() => { | |||
| setStockTakeSectionOptions((prev) => { | |||
| const sectionSet = new Set<string>(prev); | |||
| inventoryLotDetails.forEach((item) => { | |||
| const section = item.stockTakeSection?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| }); | |||
| return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| setStoreIdOptions((prev) => { | |||
| const storeSet = new Set<string>([...prev, "2F", "4F"]); | |||
| inventoryLotDetails.forEach((item) => { | |||
| const storeId = item.storeId?.trim(); | |||
| if (storeId) storeSet.add(storeId); | |||
| }); | |||
| return Array.from(storeSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| }, [inventoryLotDetails]); | |||
| return ( | |||
| <Box> | |||
| {onBack && ( | |||
| @@ -1260,11 +1258,101 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| <AccordionDetails> | |||
| <Box sx={{ width: "100%" }}> | |||
| <SearchBox<ApproverSearchKey> | |||
| criteria={approverSearchCriteria} | |||
| onSearch={handleApproverSearchBoxSearch} | |||
| onReset={handleApproverSearchBoxReset} | |||
| /> | |||
| <Card elevation={0}> | |||
| <CardContent> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} md={4}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Stock Take Section")}</InputLabel> | |||
| <Select | |||
| value={searchSectionDescription} | |||
| label={t("Stock Take Section")} | |||
| onChange={(e) => setSearchSectionDescription(e.target.value)} | |||
| > | |||
| <MenuItem value="All">{t("All")}</MenuItem> | |||
| {sectionDescriptionOptions.map((desc) => ( | |||
| <MenuItem key={desc} value={desc}> | |||
| {desc} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <Autocomplete | |||
| freeSolo | |||
| options={stockTakeSectionOptions} | |||
| value={searchStockTakeSession} | |||
| onInputChange={(_, newValue) => setSearchStockTakeSession(newValue)} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| fullWidth | |||
| label={t("Stock Take Section (can use , to search multiple sections)")} | |||
| /> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Item")} | |||
| value={searchItemKeyword} | |||
| onChange={(e) => setSearchItemKeyword(e.target.value)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Warehouse")} | |||
| value={searchWarehouseKeyword} | |||
| onChange={(e) => setSearchWarehouseKeyword(e.target.value)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Store ID")}</InputLabel> | |||
| <Select | |||
| value={searchStoreId} | |||
| label={t("Store ID")} | |||
| onChange={(e) => setSearchStoreId(e.target.value)} | |||
| > | |||
| <MenuItem value="All">{t("All")}</MenuItem> | |||
| {storeIdOptions.map((storeId) => ( | |||
| <MenuItem key={storeId} value={storeId}> | |||
| {storeId} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| {mode === "pending" && ( | |||
| <Grid item xs={12} md={4}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Record Status")}</InputLabel> | |||
| <Select | |||
| value={searchStatus} | |||
| label={t("Record Status")} | |||
| onChange={(e) => setSearchStatus(e.target.value)} | |||
| > | |||
| <MenuItem value="pending">{t("Pending")}</MenuItem> | |||
| <MenuItem value="notMatch">{t("Not Match")}</MenuItem> | |||
| <MenuItem value="pass">{t("Pass")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| )} | |||
| </Grid> | |||
| <CardActions sx={{ px: 0, pt: 2, gap: 1 }}> | |||
| <Button variant="outlined" onClick={handleResetSearch}> | |||
| {t("Reset")} | |||
| </Button> | |||
| <Button variant="contained" onClick={handleSearch}> | |||
| {t("Search")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| </Box> | |||
| </AccordionDetails> | |||
| </Accordion> | |||
| @@ -12,21 +12,24 @@ import { | |||
| CircularProgress, | |||
| TablePagination, | |||
| Grid, | |||
| LinearProgress, | |||
| Dialog, | |||
| DialogTitle, | |||
| DialogContent, | |||
| DialogContentText, | |||
| DialogActions, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| Autocomplete, | |||
| } from "@mui/material"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { useSession } from "next-auth/react"; | |||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | |||
| import { useState, useCallback, useEffect } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import duration from "dayjs/plugin/duration"; | |||
| import { | |||
| getStockTakeRecords, | |||
| AllPickedStockTakeListReponse, | |||
| createStockTakeForSections, | |||
| getStockTakeRecordsPaged, | |||
| @@ -36,7 +39,6 @@ import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; | |||
| import dayjs from "dayjs"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { AUTH } from "@/authorities"; | |||
| const PER_PAGE = 6; | |||
| interface PickerCardListProps { | |||
| /** 由父層保存,從明細返回時仍回到同一頁 */ | |||
| @@ -57,7 +59,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| const { t } = useTranslation(["inventory", "common"]); | |||
| dayjs.extend(duration); | |||
| const PER_PAGE = 6; | |||
| const [loading, setLoading] = useState(false); | |||
| const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| @@ -68,66 +69,45 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| const [listRefreshNonce, setListRefreshNonce] = useState(0); | |||
| const [creating, setCreating] = useState(false); | |||
| const [openConfirmDialog, setOpenConfirmDialog] = useState(false); | |||
| const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All"); | |||
| const [filterStockTakeSession, setFilterStockTakeSession] = useState<string>(""); | |||
| const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState<{ value: string; label: string }[]>([]); | |||
| type PickerSearchKey = "sectionDescription" | "stockTakeSession"; | |||
| const sectionDescriptionOptions = Array.from( | |||
| new Set( | |||
| stockTakeSessions | |||
| .map((s) => s.stockTakeSectionDescription) | |||
| .filter((v): v is string => !!v) | |||
| ) | |||
| ); | |||
| /* | |||
| // 按 description + section 双条件过滤 | |||
| const filteredSessions = stockTakeSessions.filter((s) => { | |||
| const matchDesc = | |||
| filterSectionDescription === "All" || | |||
| s.stockTakeSectionDescription === filterSectionDescription; | |||
| const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); | |||
| const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]); | |||
| const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All"); | |||
| const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>(""); | |||
| const [searchStatus, setSearchStatus] = useState<string>("All"); | |||
| const [searchArea, setSearchArea] = useState<string>(""); | |||
| const [searchStoreId, setSearchStoreId] = useState<string>("All"); | |||
| const sessionParts = (filterStockTakeSession ?? "") | |||
| .split(",") | |||
| .map((p) => p.trim().toLowerCase()) | |||
| .filter(Boolean); | |||
| const matchSession = | |||
| sessionParts.length === 0 || | |||
| sessionParts.some((part) => | |||
| (s.stockTakeSession ?? "").toString().toLowerCase().includes(part) | |||
| ); | |||
| const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All"); | |||
| const [filterStockTakeSession, setFilterStockTakeSession] = useState<string>(""); | |||
| const [filterStatus, setFilterStatus] = useState<string>("All"); | |||
| const [filterArea, setFilterArea] = useState<string>(""); | |||
| const [filterStoreId, setFilterStoreId] = useState<string>("All"); | |||
| return matchDesc && matchSession; | |||
| }); | |||
| */ | |||
| const statusOptions = ["pending", "stockTaking", "approving", "completed"]; | |||
| // SearchBox 的条件配置 | |||
| const criteria: Criterion<PickerSearchKey>[] = [ | |||
| { | |||
| type: "autocomplete", | |||
| label: t("Stock Take Section Description"), | |||
| paramName: "sectionDescription", | |||
| options: sectionDescriptionAutocompleteOptions, | |||
| needAll: true, | |||
| }, | |||
| { | |||
| type: "text", | |||
| label: t("Stock Take Section (can use , to search multiple sections)"), | |||
| paramName: "stockTakeSession", | |||
| placeholder: "", | |||
| }, | |||
| ]; | |||
| const handleSearch = () => { | |||
| setFilterSectionDescription(searchSectionDescription || "All"); | |||
| setFilterStockTakeSession(searchStockTakeSession || ""); | |||
| setFilterStatus(searchStatus || "All"); | |||
| setFilterArea(searchArea || ""); | |||
| setFilterStoreId(searchStoreId || "All"); | |||
| onListPageChange(0); | |||
| }; | |||
| const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => { | |||
| setFilterSectionDescription(inputs.sectionDescription || "All"); | |||
| setFilterStockTakeSession(inputs.stockTakeSession || ""); | |||
| onListPageChange(0); | |||
| }; | |||
| const handleResetSearch = () => { | |||
| setFilterSectionDescription("All"); | |||
| setFilterStockTakeSession(""); | |||
| onListPageChange(0); | |||
| }; | |||
| const handleResetSearch = () => { | |||
| setSearchSectionDescription("All"); | |||
| setSearchStockTakeSession(""); | |||
| setSearchStatus("All"); | |||
| setSearchArea(""); | |||
| setSearchStoreId("All"); | |||
| setFilterSectionDescription("All"); | |||
| setFilterStockTakeSession(""); | |||
| setFilterStatus("All"); | |||
| setFilterArea(""); | |||
| setFilterStoreId("All"); | |||
| onListPageChange(0); | |||
| }; | |||
| useEffect(() => { | |||
| let cancelled = false; | |||
| @@ -135,6 +115,9 @@ const handleResetSearch = () => { | |||
| getStockTakeRecordsPaged(page, pageSize, { | |||
| sectionDescription: filterSectionDescription, | |||
| stockTakeSections: filterStockTakeSession, | |||
| status: filterStatus, | |||
| area: filterArea, | |||
| storeId: filterStoreId, | |||
| }) | |||
| .then((res) => { | |||
| if (cancelled) return; | |||
| @@ -154,7 +137,7 @@ const handleResetSearch = () => { | |||
| return () => { | |||
| cancelled = true; | |||
| }; | |||
| }, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]); | |||
| }, [page, pageSize, filterSectionDescription, filterStockTakeSession, filterStatus, filterArea, filterStoreId, listRefreshNonce]); | |||
| //const startIdx = page * PER_PAGE; | |||
| //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | |||
| @@ -187,18 +170,44 @@ const handleResetSearch = () => { | |||
| fetchStockTakeSections() | |||
| .then((sections) => { | |||
| const descSet = new Set<string>(); | |||
| const sectionSet = new Set<string>(); | |||
| const storeIdSet = new Set<string>(["2F", "4F"]); | |||
| sections.forEach((s) => { | |||
| const section = s.stockTakeSection?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| const desc = s.stockTakeSectionDescription?.trim(); | |||
| if (desc) descSet.add(desc); | |||
| const storeId = s.storeId?.trim(); | |||
| if (storeId) storeIdSet.add(storeId); | |||
| }); | |||
| setSectionDescriptionAutocompleteOptions( | |||
| Array.from(descSet).map((desc) => ({ value: desc, label: desc })) | |||
| ); | |||
| setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); | |||
| setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); | |||
| setStoreIdOptions(Array.from(storeIdSet).sort((a, b) => a.localeCompare(b))); | |||
| }) | |||
| .catch((e) => { | |||
| console.error("Failed to load section descriptions for filter:", e); | |||
| }); | |||
| }, []); | |||
| useEffect(() => { | |||
| setStockTakeSectionOptions((prev) => { | |||
| const sectionSet = new Set<string>(prev); | |||
| stockTakeSessions.forEach((item) => { | |||
| const section = item.stockTakeSession?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| }); | |||
| return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| }, [stockTakeSessions]); | |||
| useEffect(() => { | |||
| setStoreIdOptions((prev) => { | |||
| const storeIdSet = new Set<string>([...prev, "2F", "4F"]); | |||
| stockTakeSessions.forEach((item) => { | |||
| const storeId = item.storeId?.trim(); | |||
| if (storeId) storeIdSet.add(storeId); | |||
| }); | |||
| return Array.from(storeIdSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| }, [stockTakeSessions]); | |||
| const getStatusColor = (status: string) => { | |||
| const statusLower = status.toLowerCase(); | |||
| if (statusLower === "completed") return "success"; | |||
| @@ -277,23 +286,99 @@ const handleResetSearch = () => { | |||
| if (!first?.planStartDate) return null; | |||
| return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT); | |||
| })(); | |||
| if (loading) { | |||
| return ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ); | |||
| } | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ width: "100%", mb: 2 }}> | |||
| <SearchBox<PickerSearchKey> | |||
| criteria={criteria} | |||
| onSearch={handleSearch} | |||
| onReset={handleResetSearch} | |||
| /> | |||
| </Box> | |||
| <Card elevation={0} sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <Typography variant="overline" sx={{ display: "block", mb: 1 }}> | |||
| {t("Search Criteria")} | |||
| </Typography> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12} md={4}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Stock Take Section")}</InputLabel> | |||
| <Select | |||
| value={searchSectionDescription} | |||
| label={t("Stock Take Section")} | |||
| onChange={(e) => setSearchSectionDescription(e.target.value)} | |||
| > | |||
| <MenuItem value="All">{t("All")}</MenuItem> | |||
| {sectionDescriptionOptions.map((desc) => ( | |||
| <MenuItem key={desc} value={desc}> | |||
| {desc} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <Autocomplete | |||
| freeSolo | |||
| options={stockTakeSectionOptions} | |||
| value={searchStockTakeSession} | |||
| onInputChange={(_, newValue) => setSearchStockTakeSession(newValue)} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| fullWidth | |||
| label={t("Stock Take Section (can use , to search multiple sections)")} | |||
| /> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} md={4}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Status")}</InputLabel> | |||
| <Select | |||
| value={searchStatus} | |||
| label={t("Status")} | |||
| onChange={(e) => setSearchStatus(e.target.value)} | |||
| > | |||
| <MenuItem value="All">{t("All")}</MenuItem> | |||
| {statusOptions.map((status) => ( | |||
| <MenuItem key={status} value={status}> | |||
| {t(status)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Area")} | |||
| value={searchArea} | |||
| onChange={(e) => setSearchArea(e.target.value)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Store ID")}</InputLabel> | |||
| <Select | |||
| value={searchStoreId} | |||
| label={t("Store ID")} | |||
| onChange={(e) => setSearchStoreId(e.target.value)} | |||
| > | |||
| <MenuItem value="All">{t("All")}</MenuItem> | |||
| {storeIdOptions.map((storeId) => ( | |||
| <MenuItem key={storeId} value={storeId}> | |||
| {storeId} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| <CardActions sx={{ px: 0, pt: 2, gap: 1 }}> | |||
| <Button variant="outlined" onClick={handleResetSearch}> | |||
| {t("Reset")} | |||
| </Button> | |||
| <Button variant="contained" onClick={handleSearch}> | |||
| {t("Search")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | |||
| @@ -314,13 +399,23 @@ const handleResetSearch = () => { | |||
| </Button> | |||
| </Box> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <Grid container spacing={2}> | |||
| {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => { | |||
| {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => { | |||
| const statusColor = getStatusColor(session.status || ""); | |||
| const lastStockTakeDate = session.lastStockTakeDate | |||
| ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) | |||
| : "-"; | |||
| const completionRate = getCompletionRate(session); | |||
| const sectionMeta = [ | |||
| session.stockTakeSectionDescription, | |||
| session.warehouseArea, | |||
| session.storeId, | |||
| ].filter((v): v is string => Boolean(v && v.trim())).join(" / "); | |||
| return ( | |||
| <Grid key={session.id} item xs={12} sm={6} md={4}> | |||
| @@ -337,7 +432,7 @@ const handleResetSearch = () => { | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | |||
| <Typography variant="subtitle1" fontWeight={600}> | |||
| {t("Section")}: {session.stockTakeSession} | |||
| {session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null} | |||
| {sectionMeta ? ` (${sectionMeta})` : null} | |||
| </Typography> | |||
| </Stack> | |||
| @@ -383,6 +478,7 @@ const handleResetSearch = () => { | |||
| ); | |||
| })} | |||
| </Grid> | |||
| )} | |||
| {total > 0 && ( | |||
| <TablePagination | |||
| @@ -495,6 +495,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| {/* | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| @@ -520,6 +521,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| */} | |||
| <Typography variant="body2"> | |||
| = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))} | |||
| </Typography> | |||
| @@ -528,7 +530,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| <Typography variant="body2"> | |||
| {t("First")}:{" "} | |||
| {formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "} | |||
| ({formatNumber(detail.firstBadQty ?? 0)}) ={" "} | |||
| {/* ({formatNumber(detail.firstBadQty ?? 0)}) */} | |||
| ={" "} | |||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| @@ -562,6 +565,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| {/* | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| @@ -587,6 +591,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| */} | |||
| <Typography variant="body2"> | |||
| = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))} | |||
| </Typography> | |||
| @@ -595,7 +600,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| <Typography variant="body2"> | |||
| {t("Second")}:{" "} | |||
| {formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "} | |||
| ({formatNumber(detail.secondBadQty ?? 0)}) ={" "} | |||
| {/* ({formatNumber(detail.secondBadQty ?? 0)}) */} | |||
| ={" "} | |||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| @@ -191,18 +191,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| 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") | |||
| : t("Please enter Second QTY"), | |||
| "error" | |||
| ); | |||
| return; | |||
| } | |||
| const totalQty = parseFloat(totalQtyStr); | |||
| const totalQty = parseFloat(totalQtyStr || "0") || 0; | |||
| const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0 | |||
| if (Number.isNaN(totalQty)) { | |||
| @@ -617,6 +608,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| {/* | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| @@ -643,6 +635,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| */} | |||
| <Typography variant="body2"> | |||
| = | |||
| {formatNumber( | |||
| @@ -658,11 +651,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| (detail.firstStockTakeQty ?? 0) + | |||
| (detail.firstBadQty ?? 0) | |||
| )}{" "} | |||
| {/* | |||
| ( | |||
| {formatNumber( | |||
| detail.firstBadQty ?? 0 | |||
| )} | |||
| ) ={" "} | |||
| */} | |||
| ={" "} | |||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| @@ -693,6 +688,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| /> | |||
| {/* | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| @@ -715,6 +711,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| /> | |||
| */} | |||
| <Typography variant="body2"> | |||
| = | |||
| {formatNumber( | |||
| @@ -730,11 +727,13 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| (detail.secondStockTakeQty ?? 0) + | |||
| (detail.secondBadQty ?? 0) | |||
| )}{" "} | |||
| {/* | |||
| ( | |||
| {formatNumber( | |||
| detail.secondBadQty ?? 0 | |||
| )} | |||
| ) ={" "} | |||
| */} | |||
| ={" "} | |||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| @@ -110,7 +110,7 @@ export const REPORTS: ReportDefinition[] = [ | |||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance-v2`, | |||
| fields: [ | |||
| { | |||
| label: "盤點輪次 Stock Take Round", | |||
| label: "盤點輪次", | |||
| name: "stockTakeRoundId", | |||
| type: "select", | |||
| required: true, | |||
| @@ -118,7 +118,31 @@ export const REPORTS: ReportDefinition[] = [ | |||
| dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-take-rounds`, | |||
| options: [] | |||
| }, | |||
| { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, | |||
| { label: "物料編號", name: "itemCode", type: "text", required: false}, | |||
| { | |||
| label: "倉庫樓層", | |||
| name: "store_id", | |||
| type: "select", | |||
| required: false, | |||
| options: [ | |||
| { label: "全部", value: "All" }, | |||
| { label: "1F", value: "1F" }, | |||
| { label: "2F", value: "2F" }, | |||
| { label: "3F", value: "3F" }, | |||
| { label: "4F", value: "4F" } | |||
| ], | |||
| }, | |||
| { | |||
| label: "狀態", | |||
| name: "status", | |||
| type: "select", | |||
| required: false, | |||
| options: [ | |||
| { label: "全部", value: "All" }, | |||
| { label: "待盤點", value: "pending" }, | |||
| { label: "已審核", value: "completed" } | |||
| ], | |||
| }, | |||
| ] | |||
| }, | |||
| { id: "rep-011", | |||
| @@ -15,8 +15,6 @@ | |||
| "Floor": "樓層", | |||
| "Job Order Type": "工單類型", | |||
| "FG": "成品", | |||
| "WIP": "半成品", | |||
| "BOM Type": "BOM 類型", | |||
| "No Lot": "沒有批號", | |||
| "Select All": "全選", | |||
| @@ -110,7 +108,7 @@ | |||
| "code": "編號", | |||
| "Name": "名稱", | |||
| "Assignment successful": "分配成功", | |||
| "Pass": "通過", | |||
| "Unable to get user ID": "無法獲取用戶ID", | |||
| "Unknown error: ": "未知錯誤: ", | |||
| "Please try again later.": "請稍後重試。", | |||
| @@ -468,8 +466,8 @@ | |||
| "Powder_Mixture": "箱料粉", | |||
| "Powder Mixture": "箱料粉", | |||
| "Not Match": "數值不符", | |||
| "Pass": "通過", | |||
| "pass": "通過", | |||
| "Pass": "已盤點", | |||
| "pass": "已盤點", | |||
| "Actions": "操作", | |||
| "Insert": "插入", | |||
| "Move to order": "移動到指定順序", | |||
| @@ -50,11 +50,13 @@ | |||
| "Batch Save All": "批量保存所有", | |||
| "not match": "數值不符", | |||
| "Not Match": "數值不符", | |||
| "Pass": "通過", | |||
| "Pass": "已盤點", | |||
| "Area": "區域", | |||
| "Selected Qty": "選擇數量", | |||
| "Show Search Filters": "顯示搜索器", | |||
| "Hide Search Filters": "隱藏搜索器", | |||
| "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數(含壞品)= 可用數", | |||
| "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數= 可用數", | |||
| "View ReStockTake": "查看重新盤點", | |||
| "Stock Take Qty": "盤點數", | |||
| "variance Percentage": "差異百分比", | |||
| @@ -97,9 +99,11 @@ | |||
| "book qty": "帳面庫存", | |||
| "start time": "開始時間", | |||
| "end time": "結束時間", | |||
| "notmatch": "數值不符", | |||
| "Only Variance": "僅差異", | |||
| "Control Time": "操作時間", | |||
| "pass": "通過", | |||
| "Batch approver save completed: {{success}} success, {{errors}} errors": "批次審核儲存完成:成功 {{success}} 筆,錯誤 {{errors}} 筆", | |||
| "pass": "已盤點", | |||
| "not pass": "不通過", | |||
| "Available": "可用", | |||
| "approving": "審核中", | |||
| @@ -126,6 +130,7 @@ | |||
| "Create Stock Take for All Sections": "為所有區域創建盤點", | |||
| "section": "區域", | |||
| "Stock Take Section": "盤點區域", | |||
| "Store ID":"樓層", | |||
| "Warehouse Location": "倉庫位置", | |||
| "UOM": "單位", | |||
| "First Qty": "第一次盤點數量", | |||