diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 2cc0bdc..d34c3a9 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -993,6 +993,26 @@ export const fetchConsumableWorkbenchPickOrderLotsHierarchical = cache(async (us } }); +/** Bust pickorder cache then reload consumable workbench hierarchy (after scan-pick). */ +export async function reloadConsumableWorkbenchPickOrderLotsHierarchical(userId: number): Promise { + revalidateTag("pickorder"); + try { + return await serverFetchJson( + `${BASE_API_URL}/pickOrder/workbench/all-lots-hierarchical/${userId}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); + } catch (error) { + console.error("❌ Error reloading consumable workbench hierarchical lot details:", error); + return { + fgInfo: null, + pickOrders: [], + }; + } +} + /** * Workbench assign: assign by delivery_order_pick_order id. */ diff --git a/src/components/PickOrderSearch/WorkbenchPickExecution.tsx b/src/components/PickOrderSearch/WorkbenchPickExecution.tsx index b89a416..c0d3d6c 100644 --- a/src/components/PickOrderSearch/WorkbenchPickExecution.tsx +++ b/src/components/PickOrderSearch/WorkbenchPickExecution.tsx @@ -25,15 +25,13 @@ import { useSession } from "next-auth/react"; import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; -import SearchBox, { Criterion } from "../SearchBox"; import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; import { SessionWithTokens } from "@/config/authConfig"; import { - fetchPickOrderWithStockClient, - fetchWorkbenchPickOrderLineDetailV2, + fetchConsumableWorkbenchPickOrderLotsHierarchical, + reloadConsumableWorkbenchPickOrderLotsHierarchical, confirmLotSubstitution, suggestPickOrderWorkbenchV2, - type PickOrderLotDetailResponse, } from "@/app/api/pickOrder/actions"; import { workbenchScanPick } from "@/app/api/doworkbench/actions"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; @@ -48,6 +46,7 @@ import { isExpiredWorkbenchReminderMessage, isInventoryLotLineUnavailable, isLotAvailabilityExpired, + isNoLotWorkbenchRow, isWorkbenchSourceLotExpired, isWorkbenchZeroCompleteLot, translateWorkbenchRejectMessage, @@ -56,19 +55,30 @@ import { dayjs.extend(arraySupport); -type TopRow = { +type LineRow = { rowKey: string; pickOrderId: number; pickOrderLineId: number; pickOrderCode: string; itemCode: string; itemName: string; + itemId?: number; requiredQty: number; - currentStock: number; pickedQty: number; stockUnit: string; - targetDate: string | number[]; status: string; + lotsRaw: unknown[]; +}; + +type PickOrderTopRow = { + rowKey: string; + pickOrderId: number; + pickOrderCode: string; + targetDate: string; + status: string; + lineCount: number; + completedLineCount: number; + lines: LineRow[]; }; type LotRow = { @@ -80,11 +90,13 @@ type LotRow = { itemName: string; uomDesc: string; requiredQty: number; + pickOrderLineRequiredQty?: number; availableQty: number; itemTotalAvailableQty?: number | null; stockOutLineId: number; status: string; pickedQty: number; + stockOutLineQty?: number; lotNo: string; location: string; itemId?: number; @@ -93,6 +105,7 @@ type LotRow = { lotAvailability?: string; lotStatus?: string; expiryDate?: string; + noLot?: boolean; stockOutLineRejectMessage?: string; }; @@ -235,53 +248,105 @@ function safeDisplayTargetDate(targetDate: string | number[]): string { } } -function lineHasStockOutOrSuggestion(details: PickOrderLotDetailResponse[]): boolean { - if (!details.length) return false; - return details.some((d) => { - const sol = toNum(d.stockOutLineId); - const spl = toNum(d.suggestedPickLotId); - return sol > 0 || spl > 0 || d.noLot === true; +function lineHierarchicalNeedsSuggest(line: LineRow): boolean { + if (isCompletedStatus(line.status)) return false; + if (!line.lotsRaw.length) return true; + return line.lotsRaw.every((lotRaw) => { + const lot = lotRaw as Record; + return toNum(lot.stockOutLineId) <= 0; + }); +} + +function flattenLotsFromPickOrder(po: PickOrderTopRow): LotRow[] { + const rows: LotRow[] = []; + po.lines.forEach((line) => { + line.lotsRaw.forEach((lotRaw, i) => { + const lot = lotRaw as Record; + const solId = toNum(lot.stockOutLineId); + const lotId = toNum(lot.id ?? lot.lotId, i); + const stockInLineId = toNum(lot.stockInLineId); + rows.push({ + key: solId > 0 ? `sol:${solId}` : `line:${line.pickOrderLineId}:lot:${lotId}:${i}`, + pickOrderId: po.pickOrderId, + pickOrderLineId: line.pickOrderLineId, + pickOrderCode: po.pickOrderCode, + itemCode: line.itemCode, + itemName: line.itemName, + uomDesc: toStr(lot.stockUnit) || line.stockUnit, + requiredQty: toNum(lot.requiredQty, line.requiredQty), + pickOrderLineRequiredQty: line.requiredQty, + availableQty: toNum(lot.availableQty), + stockOutLineId: solId, + status: toStr(lot.stockOutLineStatus ?? lot.processingStatus ?? "pending"), + pickedQty: toNum(lot.actualPickQty ?? lot.stockOutLineQty), + stockOutLineQty: toNum(lot.stockOutLineQty ?? lot.actualPickQty), + lotNo: toStr(lot.lotNo), + location: toStr(lot.location), + itemId: line.itemId, + stockInLineId: stockInLineId > 0 ? stockInLineId : undefined, + suggestedPickLotId: toNum(lot.suggestedPickLotId) > 0 ? toNum(lot.suggestedPickLotId) : undefined, + lotAvailability: toStr(lot.lotAvailability), + lotStatus: toStr(lot.lotStatus), + expiryDate: toStr(lot.expiryDate), + noLot: lot.noLot === true || (solId > 0 && !toStr(lot.lotNo)), + }); + }); }); + return rows; +} + +function sumLinePickedQtyFromLots(lots: unknown[]): number { + return lots.reduce((acc, lot) => { + const row = lot as Record; + const st = String(row?.stockOutLineStatus ?? row?.processingStatus ?? "").toLowerCase(); + const counted = + st === "completed" || + st === "complete" || + st === "partially_completed" || + st === "partially_complete"; + if (!counted) return acc; + return acc + toNum(row?.actualPickQty ?? row?.stockOutLineQty); + }, 0); } -function mapLotDetailsToRows( - details: PickOrderLotDetailResponse[], - ctx: { - pickOrderId: number; - pickOrderLineId: number; - pickOrderCode: string; - itemCode: string; - itemName: string; - totalAvailableQty?: number | null; - }, -): LotRow[] { - return details.map((d, i) => { - const solId = toNum(d.stockOutLineId); - const lotId = toNum(d.lotId, i); - const stockInLineId = toNum(d.stockInLineId); +function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] { + const root = data as { pickOrders?: unknown[] }; + const pos = Array.isArray(root?.pickOrders) ? root.pickOrders : []; + return pos.map((poRaw) => { + const po = poRaw as Record; + const pickOrderId = toNum(po?.pickOrderId); + const pickOrderCode = toStr(po?.pickOrderCode); + const rawLines = Array.isArray(po?.pickOrderLines) ? po.pickOrderLines : []; + const lines: LineRow[] = rawLines.map((lineRaw, idx) => { + const line = lineRaw as Record; + const item = (line?.item as Record) || {}; + const lots = Array.isArray(line?.lots) ? line.lots : []; + const pickOrderLineId = toNum(line?.id, idx); + return { + rowKey: `po:${pickOrderId}:line:${pickOrderLineId}`, + pickOrderId, + pickOrderLineId, + pickOrderCode, + itemCode: toStr(item?.code), + itemName: toStr(item?.name), + itemId: toNum(item?.id) || undefined, + requiredQty: toNum(line?.requiredQty), + pickedQty: sumLinePickedQtyFromLots(lots), + stockUnit: toStr(item?.uomDesc ?? item?.uomCode), + status: toStr(line?.status), + lotsRaw: lots, + }; + }); + const completedLineCount = lines.filter((l) => isCompletedStatus(l.status)).length; return { - key: solId > 0 ? `sol:${solId}` : `lot:${lotId}:${i}`, - pickOrderId: ctx.pickOrderId, - pickOrderLineId: ctx.pickOrderLineId, - pickOrderCode: ctx.pickOrderCode, - itemCode: ctx.itemCode, - itemName: ctx.itemName, - uomDesc: toStr(d.stockUnit), - requiredQty: toNum(d.requiredQty), - availableQty: toNum(d.remainingAfterAllPickOrders ?? d.availableQty), - itemTotalAvailableQty: toNum(ctx.totalAvailableQty), - stockOutLineId: solId, - status: toStr(d.stockOutLineStatus ?? "pending"), - pickedQty: toNum(d.actualPickQty ?? d.stockOutLineQty), - lotNo: toStr(d.lotNo), - location: toStr(d.location), - itemId: toNum(d.itemId) || undefined, - stockInLineId: stockInLineId > 0 ? stockInLineId : undefined, - suggestedPickLotId: toNum(d.suggestedPickLotId) || undefined, - lotAvailability: toStr((d as any).lotAvailability), - lotStatus: toStr((d as any).lotStatus), - expiryDate: toStr((d as any).expiryDate), - stockOutLineRejectMessage: toStr((d as any).stockOutLineRejectMessage), + rowKey: `po:${pickOrderId}`, + pickOrderId, + pickOrderCode, + targetDate: toStr(po?.targetDate ?? ""), + status: toStr(po?.status), + lineCount: lines.length, + completedLineCount, + lines, }; }); } @@ -291,29 +356,21 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const { data: session } = useSession() as { data: SessionWithTokens | null }; const userId = session?.id ? parseInt(session.id, 10) : 0; - const [originalTopRows, setOriginalTopRows] = useState([]); - const [filteredTopRows, setFilteredTopRows] = useState([]); + const [pickOrders, setPickOrders] = useState([]); const [pickOrderLoading, setPickOrderLoading] = useState(false); - const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 }); - const [totalCountItems, setTotalCountItems] = useState(0); + const [poPagingController, setPoPagingController] = useState({ pageNum: 1, pageSize: 10 }); + const [totalCountPickOrders, setTotalCountPickOrders] = useState(0); const localizeBackendMessage = (msg: unknown, fallbackKey: string) => { const text = typeof msg === "string" ? msg.trim() : ""; if (!text) return t(fallbackKey); return t(text, { defaultValue: text }); }; - const [selectedPickOrderLineId, setSelectedPickOrderLineId] = useState(null); const [selectedPickOrderId, setSelectedPickOrderId] = useState(null); - const [selectedTopMeta, setSelectedTopMeta] = useState<{ - pickOrderCode: string; - itemCode: string; - itemName: string; - totalAvailableQty?: number; - } | null>(null); const [lotRows, setLotRows] = useState([]); const [qtyBySolId, setQtyBySolId] = useState>({}); const [qtyEditableBySolId, setQtyEditableBySolId] = useState>({}); - const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: 10 }); + const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: -1 }); const [loading, setLoading] = useState(false); const [submittingSolId, setSubmittingSolId] = useState(null); const [message, setMessage] = useState(""); @@ -341,12 +398,13 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const { values: qrValues, isScanning, startScan, resetScan } = useQrCodeScannerContext(); - const paginatedTopRows = useMemo(() => { - const start = (pagingController.pageNum - 1) * pagingController.pageSize; - return filteredTopRows.slice(start, start + pagingController.pageSize); - }, [filteredTopRows, pagingController]); + const paginatedPickOrders = useMemo(() => { + const start = (poPagingController.pageNum - 1) * poPagingController.pageSize; + return pickOrders.slice(start, start + poPagingController.pageSize); + }, [pickOrders, poPagingController]); const paginatedLotRows = useMemo(() => { + if (lotPagingController.pageSize === -1) return lotRows; const start = lotPagingController.pageNum * lotPagingController.pageSize; return lotRows.slice(start, start + lotPagingController.pageSize); }, [lotRows, lotPagingController]); @@ -388,57 +446,25 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { return { byItemId, byStockInLineId, activeLotsByItemId }; }, [lotRows]); - const fetchNewPageItems = useCallback( - async (paging: { pageNum: number; pageSize: number }, extra: Record) => { - if (!userId) return; + const loadHierarchicalPickOrders = useCallback( + async (reload = false) => { + if (!userId) return [] as PickOrderTopRow[]; setPickOrderLoading(true); setError(""); try { - const params = { - ...extra, - pageNum: 0, - pageSize: 9999, - status: "released", - type: "consumable", - assignTo: userId, - }; - const res = await fetchPickOrderWithStockClient(params); - const records = Array.isArray(res?.records) ? res.records : []; - const rows: TopRow[] = records.flatMap((r: any) => { - const pickOrderId = toNum(r?.id); - const code = toStr(r?.code); - const status = toStr(r?.status); - const targetDate = r?.targetDate; - const lines = Array.isArray(r?.pickOrderLines) ? r.pickOrderLines : []; - return lines.map((line: any, idx: number) => ({ - rowKey: `po:${pickOrderId}:line:${toNum(line?.id, idx)}`, - pickOrderId, - pickOrderLineId: toNum(line?.id), - pickOrderCode: code, - itemCode: toStr(line?.itemCode), - itemName: toStr(line?.itemName), - requiredQty: toNum(line?.requiredQty), - currentStock: toNum(line?.availableQty), - pickedQty: toNum(line?.pickedQty), - stockUnit: toStr(line?.uomDesc ?? line?.uomShortDesc), - targetDate: targetDate ?? "", - status, - })); - }); - setOriginalTopRows(rows); - setFilteredTopRows(rows); - const pageSize = paging.pageSize || 10; - const pageNum = paging.pageNum || 1; - setTotalCountItems(rows.length); - setPagingController({ pageNum, pageSize }); - return rows; + const data = reload + ? await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId) + : await fetchConsumableWorkbenchPickOrderLotsHierarchical(userId); + const orders = mapHierarchicalToPickOrders(data); + setPickOrders(orders); + setTotalCountPickOrders(orders.length); + return orders; } catch (e) { console.error(e); setError(t("Load released pick orders failed")); - setOriginalTopRows([]); - setFilteredTopRows([]); - setTotalCountItems(0); - return [] as TopRow[]; + setPickOrders([]); + setTotalCountPickOrders(0); + return [] as PickOrderTopRow[]; } finally { setPickOrderLoading(false); } @@ -446,125 +472,62 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { [t, userId], ); - const refreshReleasedTopRowsAfterMutation = useCallback(async () => { - const latestRows = - (await fetchNewPageItems( - pagingController, - (filterArgs || {}) as Record, - )) || []; - if ( - selectedPickOrderLineId != null && - !latestRows.some((r) => r.pickOrderLineId === selectedPickOrderLineId) - ) { - setSelectedPickOrderLineId(null); - setSelectedPickOrderId(null); - setSelectedTopMeta(null); - setLotRows([]); - setQtyBySolId({}); - setQtyEditableBySolId({}); - setLotPagingController({ pageNum: 0, pageSize: 10 }); - } - }, [fetchNewPageItems, filterArgs, pagingController, selectedPickOrderLineId]); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { label: t("Item Code"), paramName: "itemCode", type: "text" }, - { label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" }, - { label: t("Item Name"), paramName: "itemName", type: "text" }, - { label: t("Target Date From"), label2: t("Target Date To"), paramName: "targetDate", type: "dateRange" }, - ], - [t], - ); - - const handleSearch = useCallback( - (query: Record) => { - const filtered = originalTopRows.filter((row) => { - const itemCodeMatch = !query.itemCode || row.itemCode.toLowerCase().includes(query.itemCode.toLowerCase()); - const pickOrderCodeMatch = - !query.pickOrderCode || row.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase()); - const itemNameMatch = !query.itemName || row.itemName.toLowerCase().includes(query.itemName.toLowerCase()); - const targetDate = Array.isArray(row.targetDate) - ? arrayToDayjs(row.targetDate) - : dayjs(typeof row.targetDate === "string" ? row.targetDate : ""); - let dateMatch = true; - if (query.targetDate || query.targetDateTo) { - const fromDate = query.targetDate ? dayjs(query.targetDate) : null; - const toDate = query.targetDateTo ? dayjs(query.targetDateTo) : null; - if (targetDate.isValid()) { - if (fromDate && fromDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(fromDate, "day") || targetDate.isAfter(fromDate, "day")); - if (toDate && toDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(toDate, "day") || targetDate.isBefore(toDate, "day")); - } - } - return itemCodeMatch && pickOrderCodeMatch && itemNameMatch && dateMatch; - }); - setFilteredTopRows(filtered); - setTotalCountItems(filtered.length); - setPagingController((prev) => ({ ...prev, pageNum: 1 })); - }, - [originalTopRows], - ); - - const handleReset = useCallback(() => { - setFilteredTopRows(originalTopRows); - setTotalCountItems(originalTopRows.length); - setPagingController((prev) => ({ ...prev, pageNum: 1 })); - }, [originalTopRows]); + const applyLotsFromPickOrder = useCallback((po: PickOrderTopRow) => { + setLotRows(flattenLotsFromPickOrder(po)); + setQtyEditableBySolId({}); + }, []); - useEffect(() => { - if (userId) fetchNewPageItems(pagingController, (filterArgs || {}) as Record); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userId, filterArgs, fetchNewPageItems]); - - const loadLineDetailV2 = useCallback( - async ( - pickOrderId: number, - pickOrderLineId: number, - meta: { - pickOrderCode: string; - itemCode: string; - itemName: string; - totalAvailableQty?: number; - }, - ) => { - if (!userId || pickOrderLineId <= 0) return; + const ensurePickOrderLotsIfNeeded = useCallback( + async (po: PickOrderTopRow) => { + if (!userId || !po.lines.some(lineHierarchicalNeedsSuggest)) return; setLoading(true); setError(""); - setMessage(""); try { - let details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId); - let list = Array.isArray(details) ? details : []; - if (!lineHasStockOutOrSuggestion(list)) { - const suggestRes = await suggestPickOrderWorkbenchV2(pickOrderId, userId); - if (suggestRes.code !== "SUCCESS") { - setError(t("Suggest pick failed")); - setLotRows([]); - return; - } - details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId); - list = Array.isArray(details) ? details : []; + const suggestRes = await suggestPickOrderWorkbenchV2(po.pickOrderId, userId); + if (suggestRes.code !== "SUCCESS") { + setError(t("Suggest pick failed")); + return; } - setLotRows( - mapLotDetailsToRows(list, { - pickOrderId, - pickOrderLineId, - pickOrderCode: meta.pickOrderCode, - itemCode: meta.itemCode, - itemName: meta.itemName, - totalAvailableQty: meta.totalAvailableQty, - }), - ); - setQtyEditableBySolId({}); + const data = await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId); + const orders = mapHierarchicalToPickOrders(data); + setPickOrders(orders); + setTotalCountPickOrders(orders.length); + const refreshed = orders.find((p) => p.pickOrderId === po.pickOrderId); + if (refreshed) applyLotsFromPickOrder(refreshed); } catch (e) { console.error(e); setError(t("Load workbench data failed")); - setLotRows([]); } finally { setLoading(false); } }, - [t, userId], + [applyLotsFromPickOrder, t, userId], ); + const refreshWorkbenchAfterMutation = useCallback(async () => { + const latestOrders = (await loadHierarchicalPickOrders(true)) || []; + if ( + selectedPickOrderId != null && + !latestOrders.some((p) => p.pickOrderId === selectedPickOrderId) + ) { + setSelectedPickOrderId(null); + setLotRows([]); + setQtyBySolId({}); + setQtyEditableBySolId({}); + setLotPagingController({ pageNum: 0, pageSize: -1 }); + return; + } + if (selectedPickOrderId != null) { + const po = latestOrders.find((p) => p.pickOrderId === selectedPickOrderId); + if (po) applyLotsFromPickOrder(po); + } + }, [applyLotsFromPickOrder, loadHierarchicalPickOrders, selectedPickOrderId]); + + useEffect(() => { + if (userId) void loadHierarchicalPickOrders(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userId, filterArgs, loadHierarchicalPickOrders]); + const submitRow = useCallback( async (row: LotRow, forceQty?: number) => { if (!userId) return; @@ -605,13 +568,10 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { setQrScanError(false); setQrScanSuccess(true); }); - if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) { - await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta); - } setWorkbenchLotLabelModalOpen(false); setWorkbenchLotLabelContextLot(null); setWorkbenchLotLabelInitialPayload(null); - await refreshReleasedTopRowsAfterMutation(); + await refreshWorkbenchAfterMutation(); } catch (e) { console.error(e); setError(t("Scan pick failed")); @@ -624,7 +584,7 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { setSubmittingSolId(null); } }, - [qtyBySolId, loadLineDetailV2, refreshReleasedTopRowsAfterMutation, selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta, t, userId], + [qtyBySolId, refreshWorkbenchAfterMutation, t, userId], ); const hasQtyOverrideBySolId = useCallback( @@ -638,6 +598,15 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { if (typeof override === "number" && Number.isFinite(override) && override >= 0) { return override; } + const st = String(lot.status || "").toLowerCase(); + if ( + st === "completed" || + st === "partially_completed" || + st === "partially_complete" + ) { + return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0); + } + if (isWorkbenchZeroCompleteLot(lot)) return 0; return Number(lot.requiredQty) || 0; }, [qtyBySolId], @@ -657,6 +626,14 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const resolveLockedSubmitQtyDisplay = useCallback( (lot: LotRow): number => { if (isWorkbenchZeroCompleteLot(lot)) return 0; + const st = String(lot.status || "").toLowerCase(); + if ( + st === "completed" || + st === "partially_completed" || + st === "partially_complete" + ) { + return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0); + } return resolveSingleSubmitQty(lot); }, [resolveSingleSubmitQty], @@ -686,6 +663,8 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const lotNo = String(row.lotNo || "").trim(); const isZeroComplete = isWorkbenchZeroCompleteLot(row); + const isNoLot = isNoLotWorkbenchRow(row); + const isUnavailable = isInventoryLotLineUnavailable(row); const hasExplicitOverride = hasQtyOverrideBySolId(row.stockOutLineId); const explicitQty = hasExplicitOverride ? Number(qtyBySolId[row.stockOutLineId]) : NaN; const qtyPayload = workbenchScanPickQtyFromLot(row); @@ -693,6 +672,8 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const canPostScanPick = isZeroComplete || + isNoLot || + isUnavailable || (lotNo !== "" && ((hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) || (wbJustQty != null && wbJustQty > 0))); @@ -709,7 +690,10 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { } const qtyToSend = - isZeroComplete || (hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) + isZeroComplete || + isNoLot || + isUnavailable || + (hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) ? 0 : Number(wbJustQty); @@ -718,41 +702,28 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { [hasQtyOverrideBySolId, qtyBySolId, submitRow, t, workbenchScanPickQtyFromLot], ); - const handleLineSelect = useCallback( - async (row: TopRow, checked: boolean) => { + const handlePickOrderSelect = useCallback( + async (row: PickOrderTopRow, checked: boolean) => { if (!checked) { - if (selectedPickOrderLineId === row.pickOrderLineId) { - setSelectedPickOrderLineId(null); + if (selectedPickOrderId === row.pickOrderId) { setSelectedPickOrderId(null); - setSelectedTopMeta(null); setLotRows([]); setQtyBySolId({}); setQtyEditableBySolId({}); - setLotPagingController({ pageNum: 0, pageSize: 10 }); + setLotPagingController({ pageNum: 0, pageSize: -1 }); } return; } - setSelectedPickOrderLineId(row.pickOrderLineId); setSelectedPickOrderId(row.pickOrderId); - setSelectedTopMeta({ - pickOrderCode: row.pickOrderCode, - itemCode: row.itemCode, - itemName: row.itemName, - totalAvailableQty: row.currentStock, - }); setLotRows([]); setQtyBySolId({}); setQtyEditableBySolId({}); - setLotPagingController({ pageNum: 0, pageSize: 10 }); + setLotPagingController({ pageNum: 0, pageSize: -1 }); setMessage(""); - await loadLineDetailV2(row.pickOrderId, row.pickOrderLineId, { - pickOrderCode: row.pickOrderCode, - itemCode: row.itemCode, - itemName: row.itemName, - totalAvailableQty: row.currentStock, - }); + applyLotsFromPickOrder(row); + await ensurePickOrderLotsIfNeeded(row); }, - [loadLineDetailV2, selectedPickOrderLineId], + [applyLotsFromPickOrder, ensurePickOrderLotsIfNeeded, selectedPickOrderId], ); const openWorkbenchLotLabelModalForLot = useCallback( @@ -854,20 +825,15 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { if (res.code !== "SUCCESS") { throw new Error((res.message as string) || t("Scan pick failed")); } - if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) { - await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta); - } + await refreshWorkbenchAfterMutation(); setWorkbenchLotLabelModalOpen(false); setWorkbenchLotLabelReminderText(null); setWorkbenchLotLabelContextLot(null); setWorkbenchLotLabelInitialPayload(null); }, [ - loadLineDetailV2, + refreshWorkbenchAfterMutation, resolveLockedSubmitQtyDisplay, - selectedPickOrderId, - selectedPickOrderLineId, - selectedTopMeta, t, userId, workbenchLotLabelContextLot, @@ -1047,10 +1013,7 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { setQrScanSuccess(true); setQrScanSuccessMsg(t("Scan pick success")); }); - if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) { - await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta); - } - await refreshReleasedTopRowsAfterMutation(); + await refreshWorkbenchAfterMutation(); clearLotConfirmationState(true); } catch (e) { console.error(e); @@ -1069,12 +1032,8 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { [ clearLotConfirmationState, expectedLotData, - loadLineDetailV2, - refreshReleasedTopRowsAfterMutation, + refreshWorkbenchAfterMutation, scannedLotData, - selectedPickOrderId, - selectedPickOrderLineId, - selectedTopMeta, t, userId, workbenchScanPickQtyFromLot, @@ -1347,11 +1306,11 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { }, [isScanning, startScan, userId]); useEffect(() => { - if (!selectedPickOrderLineId) { + if (!selectedPickOrderId) { lastProcessedQrRef.current = ""; processedQrCodesRef.current.clear(); } - }, [selectedPickOrderLineId]); + }, [selectedPickOrderId]); useEffect(() => { if (!qrValues.length || lotRows.length === 0) return; @@ -1443,7 +1402,9 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { - + + {t("Pick Orders")} + {pickOrderLoading ? ( @@ -1455,41 +1416,33 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { {t("Selected")} {t("Pick Order Code")} - {t("Item Code")} - {t("Item Name")} - {t("Order Quantity")} - {t("Current Stock")} - {t("Picked Qty")} - {t("Stock Unit")} + {t("Pick Order Lines")} {t("Target Date")} {t("Status")} - {paginatedTopRows.length === 0 ? ( + {paginatedPickOrders.length === 0 ? ( - + {t("No data available")} ) : ( - paginatedTopRows.map((row) => ( + paginatedPickOrders.map((row) => ( void handleLineSelect(row, checked)} + checked={selectedPickOrderId === row.pickOrderId} + onChange={(_, checked) => void handlePickOrderSelect(row, checked)} /> {row.pickOrderCode || "-"} - {row.itemCode || "-"} - {row.itemName || "-"} - {row.requiredQty.toLocaleString()} - {row.currentStock.toLocaleString()} - {row.pickedQty.toLocaleString()} - {row.stockUnit || "-"} + + {row.completedLineCount}/{row.lineCount} + {safeDisplayTargetDate(row.targetDate)} {t(row.status || "-")} @@ -1503,12 +1456,14 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { setPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))} + count={totalCountPickOrders} + page={poPagingController.pageNum - 1} + rowsPerPage={poPagingController.pageSize} + onPageChange={(_e, newPage) => + setPoPagingController((prev) => ({ ...prev, pageNum: newPage + 1 })) + } onRowsPerPageChange={(e) => - setPagingController({ + setPoPagingController({ pageNum: 1, pageSize: parseInt(e.target.value, 10), }) @@ -1560,6 +1515,7 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { ? Number(qtyBySolId[r.stockOutLineId]) : lockedSubmitQty; const rowStatus = String(r.status || "").toLowerCase(); + const isNoLot = isNoLotWorkbenchRow(r); const isRowRejected = rowStatus === "rejected" || String(r.lotAvailability || "").toLowerCase() === "rejected"; const isRowExpired = @@ -1568,17 +1524,15 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { return ( - {idx === 0 ? lotPagingController.pageNum * lotPagingController.pageSize + 1 : ""} - {idx === 0 ? ( - <> - {r.itemCode || "-"}
- {r.itemName || "-"}
- {r.uomDesc || "-"} - - ) : ( - "" - )} + {lotPagingController.pageSize === -1 + ? idx + 1 + : lotPagingController.pageNum * lotPagingController.pageSize + idx + 1} +
+ + {r.itemCode || "-"}
+ {r.itemName || "-"}
+ {r.uomDesc || "-"}
{r.location || "-"} @@ -1608,7 +1562,11 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { r.lotNo ) ) : ( - "-" + + {t( + "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", + )} + )} {r.stockOutLineId > 0 ? ( @@ -1657,6 +1615,27 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { /> ); } + if ( + isNoLot && + (rowStatus === "partially_completed" || + rowStatus === "partially_complete" || + rowStatus === "completed") + ) { + return ( + + ); + } + if (isNoLot) { + return null; + } return ( = ({ filterArgs }) => { - {t("No lot rows. Select a line in the table above.")} + {t("No lot rows. Select a pick order above.")} @@ -1773,13 +1752,14 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { page={lotPagingController.pageNum} rowsPerPage={lotPagingController.pageSize} onPageChange={(_e, newPage) => setLotPagingController((prev) => ({ ...prev, pageNum: newPage }))} - onRowsPerPageChange={(e) => + onRowsPerPageChange={(e) => { + const newPageSize = parseInt(e.target.value, 10); setLotPagingController({ pageNum: 0, - pageSize: parseInt(e.target.value, 10), - }) - } - rowsPerPageOptions={[10, 25, 50]} + pageSize: newPageSize === -1 ? -1 : newPageSize, + }); + }} + rowsPerPageOptions={[10, 25, 50, -1]} labelRowsPerPage={t("Rows per page")} />
diff --git a/src/i18n/en/importBom.json b/src/i18n/en/importBom.json index 6afa90d..17fcd27 100644 --- a/src/i18n/en/importBom.json +++ b/src/i18n/en/importBom.json @@ -10,11 +10,81 @@ "Is Drink": "Is Drink", "Drink": "Drink", "Powder_Mixture": "Powder Mixture", + "Other": "Other", + "Product Type": "Product type", + "Material count": "Materials", + "Process count": "Processes", + "Preview load failed": "Failed to load preview data", + "Review import data": "Review import data", + "Attribute scales": "Attribute scales", + "No materials parsed": "No materials parsed", + "No processes parsed": "No processes parsed", + "Import code required": "Item code is required", + "Import name required": "Item name is required", + "Import output qty required": "Output quantity must be greater than 0", + "Material item code required": "Material item code is required", + "Material uom required": "Material recipe UOM is required", + "Material qty required": "Material recipe quantity must be greater than 0", + "Material item not in master": "Material item {{code}} does not exist", + "Join step not in process list": "Join step must match an existing process sequence", + "Allergic has": "Has allergen", + "Allergic none": "No allergen", + "No scale data": "No scale data", + "Back to reselect files": "Back to reselect files", + "Upload check summary": "Uploaded {{uploaded}} file(s). Check result: {{total}} total — {{correct}} passed, {{failed}} failed", + "Upload count mismatch hint": "Upload count does not match check count; duplicate file names may have been renamed with _2, _3, etc.", + "Search file name": "Search file name", + "Search code or name": "Search code or name", + "Format check issues": "Format check issues", + "Review and fix": "Review and fix", + "Issue count": "{{count}} issue(s)", + "Revalidate failed": "Re-validation failed", + "Download corrected excel": "Download corrected Excel", + "Downloading...": "Downloading…", + "Download corrected excel failed": "Failed to download corrected Excel", + "Confirm import": "Confirm import", + "Download issue log": "Download issue log Excel", + "Import completed": "Import completed", + "Import failed": "Import failed. See console for details.", + "None": "None", + "Import summary prefix": "Will import {{count}} BOM(s)", + "Import summary wip": "; {{wip}} also as WIP", + "Import summary drink": "; {{drink}} drink", + "Import summary powder": "; {{powder}} powder mixture", + "Import summary other": "; {{other}} other", + "WIP": "WIP", + "Bom Kind Mode": "Kind", + "Bom Kind FG": "FG", + "Bom Kind WIP": "WIP", + "Bom Kind Both": "both", + "File Name": "File Name", "Base Score": "Base Score", "BOM Status": "BOM Status", "BOM Status Active": "Active", "BOM Status Inactive": "Inactive", "Save Status": "Save Status", + "BOM Revision": "Revision", + "Version Compare": "Version compare", + "Version": "Version", + "Compare Version": "Compare version", + "End Compare": "End compare", + "Old Version": "Old version", + "New Version": "New version", + "Loading versions...": "Loading versions...", + "Loading compare...": "Loading compare...", + "Select two different versions to compare": "Select two different versions to compare", + "Field": "Field", + "Material Changes": "Material changes", + "Old Recipe Qty": "Old recipe qty", + "New Recipe Qty": "New recipe qty", + "Old Base Qty": "Old base qty", + "New Base Qty": "New base qty", + "Recipe Qty": "Recipe qty", + "Recipe UOM": "Recipe UOM", + "Edit Materials": "Edit materials", + "Save New Version": "Save as new version", + "Material edit creates new version hint": "Editing material qty/UOM creates a new version and activates it. Status-only changes do not create a new version.", + "Content": "Content", "Saving...": "Saving...", "Code": "BOM Code", "Name": "BOM Name", @@ -23,12 +93,14 @@ "Type": "Type", "Loading BOM Detail...": "Loading BOM Detail...", "Basic Info": "Basic Info", + "Edit Basic Info": "Edit basic info", "Edit": "Edit", "Loading...": "Loading...", "Save": "Save", "Cancel": "Cancel", "Allergic Substances": "Allergic Substances", - "Depth": "Depth", + "Depth": "Color depth", + "Depth hint": "Dark 1 → Light 5", "Float": "Float", "Density": "Density", "Time Sequence": "Time Sequence", @@ -42,6 +114,15 @@ "Stock UOM": "Stock UOM", "Sales Qty": "Sales Qty", "Sales UOM": "Sales UOM", + "Join Step": "Join step", + "Putaway Location": "Putaway location", + "Putaway Floor": "Floor", + "Putaway Warehouse": "Warehouse", + "Putaway Area": "Area", + "Putaway Slot": "Slot", + "Item Location Code": "Item location code", + "Putaway location item hint": "QC auto putaway still uses item master LocationCode; this field is stored per BOM revision.", + "Putaway edit creates new version hint": "Changing putaway location creates a new version and updates the item master location.", "Process & Equipment": "Process & Equipment", "Process Code": "Process Code", "Process Description": "Process Description", @@ -51,5 +132,21 @@ "Post Prod Time (Minutes)": "Post Prod Time (Minutes)", "Add": "Add", "Sequence": "Sequence", - "Actions": "Actions" + "Actions": "Actions", + "Edit Processes": "Edit processes", + "Byproduct": "Byproduct", + "Equipment": "Equipment", + "Equipment Description": "Equipment description", + "Equipment Name": "Equipment name", + "Not applicable": "Not applicable", + "Packaging bags": "Packaging bags", + "Select process": "Select process", + "Load process master failed": "Failed to load process/equipment master data", + "Process code required": "Process code is required on every line", + "Process not in master": "Process {{code}} is not in master data", + "Equipment description and name both required": "Equipment description and name must both be set or both empty", + "Equipment not in master": "Equipment \"{{pair}}\" is not in master data", + "Bag item not in master": "Bag item {{code}} is not in master data", + "Byproduct item not in master": "Byproduct item {{code}} does not exist", + "Item code": "Item code" } diff --git a/src/i18n/en/masterDataIssue.json b/src/i18n/en/masterDataIssue.json index 54374a1..e1bfdc8 100644 --- a/src/i18n/en/masterDataIssue.json +++ b/src/i18n/en/masterDataIssue.json @@ -5,7 +5,13 @@ "masterDataIssue_BOM_MATERIAL_MISSING_ITEM": "masterDataIssue_BOM_MATERIAL_MISSING_ITEM", "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH", "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH", - "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID", + "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM material UOM reference invalid or deleted", + "masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM code does not match linked item", + "masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM product name does not match linked item", + "masterDataIssue_col_linked_item": "Linked item", + "masterDataIssue_col_product_name": "Product name", + "masterDataIssue_line_itemCodeMismatch": "{{bom}}: should link item \"{{expected}}\", actually \"{{actual}}\"", + "masterDataIssue_line_itemNameMismatch": "{{bom}}: BOM/Excel name \"{{actual}}\", item master \"{{expected}}\"", "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES": "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES", "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT": "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT", "masterDataIssue_DELETED_BASE_UOM": "masterDataIssue_DELETED_BASE_UOM", @@ -97,5 +103,31 @@ "masterDataIssue_unit_sales": "masterDataIssue_unit_sales", "masterDataIssue_unit_stock": "masterDataIssue_unit_stock", "masterDataIssue_usedInBom": "masterDataIssue_usedInBom", - "masterDataIssue_viewDetail": "masterDataIssue_viewDetail" + "masterDataIssue_viewDetail": "masterDataIssue_viewDetail", + "masterDataIssue_align_preview": "Preview fix (align to M18)", + "masterDataIssue_align_row": "Align to M18", + "masterDataIssue_align_title": "Preview: align BOM UOM to M18", + "masterDataIssue_align_info_m18": "If M18 item master is correct: use this to align BOM units and quantities. Header output qty defaults unchanged; material sale/stock/base qty recalculate via item_uom when edited. Recipe qty is independent in v1.", + "masterDataIssue_align_info_excel": "If BOM Excel is correct: do not apply this fix; update item UOM in M18 sync instead.", + "masterDataIssue_align_summary": "Fixable: {{headers}} header(s) · {{materials}} material(s) · {{skipped}} skipped", + "masterDataIssue_align_tab_headers": "BOM header ({{count}})", + "masterDataIssue_align_tab_materials": "BOM materials ({{count}})", + "masterDataIssue_align_tab_skipped": "Skipped ({{count}})", + "masterDataIssue_align_none_headers": "No fixable BOM headers.", + "masterDataIssue_align_none_materials": "No fixable BOM materials.", + "masterDataIssue_align_field": "Field", + "masterDataIssue_align_after": "After fix", + "masterDataIssue_align_outputQty": "Output qty", + "masterDataIssue_align_recipeQty": "Recipe qty", + "masterDataIssue_align_confirm": "I confirm M18 item master is the source of truth", + "masterDataIssue_align_apply": "Apply fix", + "masterDataIssue_align_applying": "Applying…", + "masterDataIssue_align_loadFailed": "Failed to load alignment preview.", + "masterDataIssue_align_applyFailed": "Apply failed. Please try again.", + "masterDataIssue_align_not_set": "Not set", + "masterDataIssue_align_warn_1to1": "BOM sales unit cannot be converted via M18 (likely a legacy Excel import). Sale qty was prefilled 1:1 — please verify before applying.", + "masterDataIssue_align_warn_bom_qty_null": "BOM qty not set on the left ({{fields}}). Adopt the M18 values on the right, or fix Excel and re-import.", + "masterDataIssue_align_warn_field_sale": "sale qty", + "masterDataIssue_align_warn_field_stock": "stock qty", + "masterDataIssue_align_warn_field_base": "base qty" } diff --git a/src/i18n/en/navigation.json b/src/i18n/en/navigation.json index 4f6da0f..61a67c7 100644 --- a/src/i18n/en/navigation.json +++ b/src/i18n/en/navigation.json @@ -43,7 +43,7 @@ "nav.settings.qcCategory": "QC Category", "nav.settings.qcItemAll": "QC Item All", "nav.settings.shopAndTruck": "Shop And Truck", - "nav.settings.deliveryOrderFloor": "DO floor (supplier)", + "nav.settings.deliveryOrderFloor": "DO floor settings", "nav.settings.demandForecast": "Demand Forecast Setting", "nav.settings.bomWeighting": "BOM Weighting Score List", "nav.settings.masterDataIssues": "BOM / Item UOM Issues", @@ -67,7 +67,7 @@ "nav.breadcrumb.qcItemAll": "QC Item All", "nav.breadcrumb.qrCodeHandle": "QR Code Handle", "nav.breadcrumb.demandForecast": "Demand Forecast Setting", - "nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor", + "nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor Settings", "nav.breadcrumb.masterDataIssues": "BOM / Item UOM Issues", "nav.breadcrumb.equipment": "Equipment", "nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit", diff --git a/src/i18n/zh/bomWeighting.json b/src/i18n/zh/bomWeighting.json index a8f7277..86b763a 100644 --- a/src/i18n/zh/bomWeighting.json +++ b/src/i18n/zh/bomWeighting.json @@ -7,6 +7,6 @@ "Edit BOM Weighting Score": "編輯 BOM 權重得分", "Scoring Item": "評分項目", "Range": "範圍", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱" } diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index bf3d7cb..a9da700 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -45,7 +45,7 @@ "Invoice": "發票", "Invoice Date": "發票日期", "IP": "IP 地址", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Loading": "載入中...", "Loading order summary": "正在載入訂單摘要", diff --git a/src/i18n/zh/do.json b/src/i18n/zh/do.json index 9d50d73..2ef5d07 100644 --- a/src/i18n/zh/do.json +++ b/src/i18n/zh/do.json @@ -77,6 +77,7 @@ "Source DO code is required": "請輸入原送貨單編號", "Source DO must be completed": "原送貨單須為已送貨狀態", "Source DO not found": "找不到原送貨單", + "Merge Extra ticket": "合併加單", "Submit": "提交", "Target DO": "目標送貨單", "This item is already in the draft list": "此物品已在待提交列表中", diff --git a/src/i18n/zh/doWorkbench.json b/src/i18n/zh/doWorkbench.json index 647829a..367c74d 100644 --- a/src/i18n/zh/doWorkbench.json +++ b/src/i18n/zh/doWorkbench.json @@ -12,7 +12,7 @@ "Delivery Order": "送貨訂單", "Edit Delivery Order Detail": "編輯送貨訂單詳情", "DO Workbench Search": "DO Workbench 搜索", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Lot No": "批號", "Location": "位置", diff --git a/src/i18n/zh/finishedgoodmanagement.json b/src/i18n/zh/finishedgoodmanagement.json index 76c02d6..761c523 100644 --- a/src/i18n/zh/finishedgoodmanagement.json +++ b/src/i18n/zh/finishedgoodmanagement.json @@ -1,7 +1,7 @@ { "Finished Good Management": "成品出倉管理", "提料順序": "提料順序", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Search & Jump": "搜索並跳轉", "Jump": "跳轉", diff --git a/src/i18n/zh/items.json b/src/i18n/zh/items.json index 057301c..02d1815 100644 --- a/src/i18n/zh/items.json +++ b/src/i18n/zh/items.json @@ -60,7 +60,7 @@ "Item": "貨品", "Code or name": "編號或名稱", "Product": "物品", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Sales Qty": "銷售數量", "Sales UOM": "銷售單位", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 1cf4efc..b76742d 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -173,8 +173,10 @@ "Is Float": "浮沉", "Issue": "問題", "Issue Remark": "問題備註", + "Update Target Production Date": "更新目標生產日期", + "Update Required Quantity": "更新需求數量", "Item": "成品/半成品", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Item already exists in created items": "物品已存在於創建的物品中", "Items": "物品", diff --git a/src/i18n/zh/masterDataIssue.json b/src/i18n/zh/masterDataIssue.json index 622c549..349417c 100644 --- a/src/i18n/zh/masterDataIssue.json +++ b/src/i18n/zh/masterDataIssue.json @@ -53,6 +53,12 @@ "masterDataIssue_BOM_MATERIAL_BASE_UOM_MISMATCH": "BOM 原料基本單位與貨品主檔不一致", "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "BOM 原料庫存單位與貨品主檔不一致", "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM 原料 UOM 參照無效或已刪除", + "masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM 編號與關聯貨品不一致", + "masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM 產品名稱與關聯貨品不一致", + "masterDataIssue_col_linked_item": "關聯貨品", + "masterDataIssue_col_product_name": "產品名稱", + "masterDataIssue_line_itemCodeMismatch": "{{bom}}:應關聯貨品「{{expected}}」,實際為「{{actual}}」", + "masterDataIssue_line_itemNameMismatch": "{{bom}}:Excel/BOM 名稱為「{{actual}}」,貨品主檔為「{{expected}}」", "masterDataIssue_group_count": "共 {{groups}} 筆 · {{issues}} 項問題", "masterDataIssue_col_subject": "主體", "masterDataIssue_col_summary": "問題摘要", @@ -97,5 +103,31 @@ "masterDataIssue_line_pairBoth": "銷售/庫存單位應為「{{expected}}」,BOM 為「{{actual}}」", "masterDataIssue_line_pairSales": "銷售單位應為「{{expected}}」,BOM 為「{{actual}}」", "masterDataIssue_line_pairStock": "庫存單位應為「{{expected}}」,BOM 為「{{actual}}」", - "masterDataIssue_line_pairBase": "基本單位應為「{{expected}}」,BOM 為「{{actual}}」" + "masterDataIssue_line_pairBase": "基本單位應為「{{expected}}」,BOM 為「{{actual}}」", + "masterDataIssue_align_preview": "預覽修正(對齊 M18)", + "masterDataIssue_align_row": "對齊 M18", + "masterDataIssue_align_title": "預覽:BOM 單位對齊 M18", + "masterDataIssue_align_info_m18": "若 M18 貨品主檔為準:可使用本功能將 BOM 單位與數量對齊 M18。BOM 表頭產出數量預設不變;原料銷售/庫存/基本數量修改後將按 item_uom 換算聯動。配方用量可單獨修改,與銷售/庫存/基本無聯動。", + "masterDataIssue_align_info_excel": "若 BOM Excel 才是正確來源:請勿使用本功能修改 BOM,應在 M18 同步中更新該貨品的銷售/庫存單位。", + "masterDataIssue_align_summary": "可修正:BOM 表頭 {{headers}} 筆 · 原料 {{materials}} 筆 · 略過 {{skipped}} 筆", + "masterDataIssue_align_tab_headers": "BOM 總表 ({{count}})", + "masterDataIssue_align_tab_materials": "BOM 原材料 ({{count}})", + "masterDataIssue_align_tab_skipped": "無法修正 ({{count}})", + "masterDataIssue_align_none_headers": "沒有可修正的 BOM 表頭。", + "masterDataIssue_align_none_materials": "沒有可修正的 BOM 原料。", + "masterDataIssue_align_field": "欄位", + "masterDataIssue_align_after": "修正後", + "masterDataIssue_align_outputQty": "產出數量", + "masterDataIssue_align_recipeQty": "配方用量", + "masterDataIssue_align_confirm": "我已確認應以 M18 貨品主檔為準", + "masterDataIssue_align_apply": "確認套用", + "masterDataIssue_align_applying": "套用中…", + "masterDataIssue_align_loadFailed": "無法載入修正預覽。", + "masterDataIssue_align_applyFailed": "套用失敗,請稍後再試。", + "masterDataIssue_align_not_set": "未設定", + "masterDataIssue_align_warn_1to1": "BOM 銷售單位無法依 M18 換算(可能為舊 Excel 匯入錯誤單位),已暫以 1:1 預填銷售數量,請確認後再套用。", + "masterDataIssue_align_warn_bom_qty_null": "BOM 左側數量未設定({{fields}})。可採用右側 M18 建議值,或修正 Excel 後重匯。", + "masterDataIssue_align_warn_field_sale": "銷售數量", + "masterDataIssue_align_warn_field_stock": "庫存數量", + "masterDataIssue_align_warn_field_base": "基本數量" } diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index bdf6bc2..f77fe74 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -2,6 +2,8 @@ "Purchase Order": "採購訂單", "Code": "編號", "Pick Order Code": "提料單編號", + "Workbench SOL completed with qty=0 (no inventory posting)": "工作台採購訂單已完成", + "No lot rows. Select a pick order above": "沒有批次行。請選擇一個提料單。", "Item Code": "貨品編號", "OrderDate": "下單日期", "Details": "詳情", @@ -10,6 +12,7 @@ "N/A": "不適用", "Release Pick Orders": "放單", "released": "已放單", + "No lot rows. Select a pick order above.": "沒有批次行。請選擇一個提料單。", "Loading...": "載入中...", "Suggestion success": "建議成功", "Scan pick success": "掃描提料成功", diff --git a/src/i18n/zh/productionProcess.json b/src/i18n/zh/productionProcess.json index 371ce5c..70c05d3 100644 --- a/src/i18n/zh/productionProcess.json +++ b/src/i18n/zh/productionProcess.json @@ -68,7 +68,7 @@ "Invalid Stock In Line Id": "無效庫存行ID", "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度", "Item": "物品", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "物品名稱", "Job Details": "工單編號及生產產品", "Job Order": "工單", diff --git a/src/i18n/zh/qcItemAll.json b/src/i18n/zh/qcItemAll.json index 2c32cd7..4dbe6a8 100644 --- a/src/i18n/zh/qcItemAll.json +++ b/src/i18n/zh/qcItemAll.json @@ -19,10 +19,10 @@ "Qc Item": "品檢項目", "Item": "物品", "Item code not found": "物品不存在", - "Error validating item code": "驗證物品編號時發生錯誤", + "Error validating item code": "驗證貨品編號時發生錯誤", "Category": "模板", "Category Type": "模板類型", - "Enter item code to validate": "請輸入物品編號以驗證", + "Enter item code to validate": "請輸入貨品編號以驗證", "Code": "編號", "Name": "名稱", "Description": "描述", @@ -61,7 +61,7 @@ "Select Qc Category": "選擇品檢模板", "Select Qc Item": "選擇品檢項目", "Select Type": "選擇類型", - "Item Code": "物品編號", + "Item Code": "貨品編號", "Item Name": "產品名稱", "Qc Category Code": "品檢模板編號", "Qc Category Name": "品檢模板名稱", diff --git a/src/utils/workbenchPickLotUtils.ts b/src/utils/workbenchPickLotUtils.ts index 1020dcc..de0ab46 100644 --- a/src/utils/workbenchPickLotUtils.ts +++ b/src/utils/workbenchPickLotUtils.ts @@ -51,11 +51,23 @@ export function isWorkbenchSourceLotExpired( return false; } -/** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */ +/** 無批號缺口列(stock_out_line 未綁定 inventory lot line) */ +export function isNoLotWorkbenchRow( + lot: WorkbenchPickLotLike | null | undefined, +): boolean { + if (!lot) return false; + return lot.noLot === true || !lot.lotNo || String(lot.lotNo).trim() === ""; +} + +/** 過期、不可用或無批號:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */ export function isWorkbenchZeroCompleteLot( lot: WorkbenchPickLotLike | null | undefined, ): boolean { - return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot); + return ( + isLotAvailabilityExpired(lot) || + isInventoryLotLineUnavailable(lot) || + isNoLotWorkbenchRow(lot) + ); } /** Backend messages with dynamic ids — map prefix to i18n key (pickOrder namespace). */