"use client"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Alert, Box, Button, Checkbox, Chip, CircularProgress, FormControlLabel, Paper, Stack, Tab, Table, TableBody, TableCell, TableHead, TableRow, Tabs, TextField, Typography, } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers/DateCalendar"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { type Dayjs } from "dayjs"; import "dayjs/locale/zh-hk"; import { runStockLedgerFixAdj, fetchStockLedgerFixDay, fetchStockLedgerFixInventory, fetchStockLedgerFixInventoryScope, fetchStockLedgerFixLotScope, runStockLedgerFixDay, runStockLedgerFixRange, runStockLedgerFixInventory, runStockLedgerFixInventoryScope, runStockLedgerFixLotScope, searchStockLedgerFixInventory, searchStockLedgerFixLot, downloadStockLedgerFixSql, type StockLedgerFixCheckPart, type StockLedgerFixDayDetail, type StockLedgerFixInventoryPreview, type StockLedgerFixRunResponse, type StockLedgerFixScopeDetail, type StockLedgerFixSearchInventoryHit, type StockLedgerFixSearchLotHit, } from "@/app/api/stockLedgerFix/client"; const FIRST_LEDGER_DAY = dayjs("2026-03-01"); const DAY_FIX_STEPS = ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] as const; type DayFixStep = (typeof DAY_FIX_STEPS)[number]; const ALL_DAY_STEPS: Record = { "2.1": true, "2.2": true, "2.3": true, "2.4": true, "2.5": true, "2.6": true, }; const DAY_STEP_LABEL: Record = { "2.1": "2.1 lot", "2.2": "2.2 uom", "2.3": "2.3 inventoryId", "2.4": "2.4 lotQty", "2.5": "2.5 balance", "2.6": "2.6 日結", }; /** Export SQL parts (aligned with fix steps + 1.0 / 2.7). */ const EXPORT_PARTS = ["1.0", "2.1", "2.3", "ledger", "2.6", "2.7"] as const; type ExportPart = (typeof EXPORT_PARTS)[number]; const DEFAULT_EXPORT_PARTS: Record = { "1.0": false, "2.1": false, "2.3": false, ledger: true, "2.6": true, "2.7": false, }; const FULL_EXPORT_PARTS: Record = { "1.0": true, "2.1": true, "2.3": true, ledger: true, "2.6": true, "2.7": true, }; const EXPORT_PART_LABEL: Record = { "1.0": "1.0 inventory", "2.1": "2.1 軟刪流水帳+出倉列批號", "2.3": "2.3 inventoryId", ledger: "ledger lot/uom/lotQty/balance", "2.6": "2.6 日結", "2.7": "2.7 ADJ+入出倉單+調整記錄", }; function selectedExportParts(flags: Record): ExportPart[] { return EXPORT_PARTS.filter((p) => flags[p]); } function exportPartsPayload(flags: Record): string[] | undefined { const selected = selectedExportParts(flags); if (selected.length === 0) return undefined; // Always send explicit list so backend does not fall back to legacy default alone return selected; } function selectedDaySteps(flags: Record): DayFixStep[] { return DAY_FIX_STEPS.filter((s) => flags[s]); } function stepsPayload(flags: Record): string[] | undefined { const selected = selectedDaySteps(flags); if (selected.length === 0 || selected.length === DAY_FIX_STEPS.length) return undefined; return selected; } function lotLineFixNote(res: StockLedgerFixRunResponse): string { const deleted = res.deletedUnlinkedLot ?? 0; const matched = res.existingLotMatch ?? 0; const mismatched = res.existingLotMismatch ?? 0; const blankOut = res.deletedBlankOutLine ?? 0; const alignedOut = res.alignedOutLine ?? 0; if (deleted === 0 && matched === 0 && mismatched === 0 && blankOut === 0 && alignedOut === 0) return ""; return `、軟刪未連批號 ${deleted}、出倉列空而軟刪 ${blankOut}、出倉列已改跟帳本 ${alignedOut}、已有批號相符 ${matched}、不相符 ${mismatched}`; } function apiErrorMessage(e: unknown, fallback: string): string { if (e && typeof e === "object" && "response" in e) { const data = (e as { response?: { data?: unknown } }).response?.data; if (typeof data === "string" && data.trim()) { return data.trim().slice(0, 400); } if (data && typeof data === "object") { const msg = (data as { message?: unknown }).message; if (typeof msg === "string" && msg.trim()) { return msg.trim().slice(0, 400); } } } if (e instanceof Error && e.message) return e.message; return fallback; } function partVerdict( part: StockLedgerFixCheckPart, ): "correct" | "miss" | "incorrect" | "over-issue" | "can-fix" | "cannot-fix" { if (part.group === "canFix") { return part.miss > 0 || part.incorrect > 0 ? "can-fix" : "correct"; } if (part.group === "cannotFix") { return part.miss > 0 || part.incorrect > 0 ? "cannot-fix" : "correct"; } if (part.key === "overIssue") { if (part.incorrect > 0 || part.miss > 0) return "over-issue"; return "correct"; } if (part.incorrect > 0) return "incorrect"; if (part.miss > 0) return "miss"; if (part.key === "dayTable" && part.ok === 0) return "miss"; return "correct"; } function CheckPartsTable({ parts }: { parts: StockLedgerFixCheckPart[] }) { const field = parts.filter((p) => !p.group || p.group === "field"); const canFix = parts.filter( (p) => p.group === "canFix" && p.miss + p.incorrect > 0, ); const cannotFix = parts.filter( (p) => p.group === "cannotFix" && p.miss + p.incorrect > 0, ); const renderRows = (rows: StockLedgerFixCheckPart[]) => rows.map((p) => { const v = partVerdict(p); return ( {p.label} {p.ok} {p.miss} {p.incorrect} ); }); return ( 項目 狀態 正確 缺 不正確 {renderRows(field)}
{canFix.length > 0 && ( <> 可自動修(按 Fix 會處理) 原因 狀態 正確 列數 — {renderRows(canFix)}
)} {cannotFix.length > 0 && ( <> 不能自動修(Fix 不會消;舊 TKE 開新批/真超發/沒有 lot 來源) 原因 狀態 正確 — 列數 {renderRows(cannotFix)}
)}
); } const StockLedgerFixPageClient: React.FC = () => { const [tab, setTab] = useState<"day" | "inventory" | "lot">("day"); const [selected, setSelected] = useState(() => dayjs().subtract(1, "day"), ); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailError, setDetailError] = useState(null); const [fixing, setFixing] = useState(false); const [fixError, setFixError] = useState(null); const [fixMessage, setFixMessage] = useState(null); const detailInFlight = useRef(false); const fixInFlight = useRef(false); const inventoryLoadInFlight = useRef(false); const inventoryRunInFlight = useRef(false); const searchInFlight = useRef(false); const scopeInFlight = useRef(false); const exportInFlight = useRef(false); const [chainFrom, setChainFrom] = useState("2026-03-15"); const [chainTo, setChainTo] = useState(() => dayjs().subtract(1, "day").format("YYYY-MM-DD"), ); const [chainRunning, setChainRunning] = useState(false); const [chainProgress, setChainProgress] = useState(null); const [daySteps, setDaySteps] = useState>(ALL_DAY_STEPS); const [exportFrom, setExportFrom] = useState("2026-03-15"); const [exportTo, setExportTo] = useState(() => dayjs().subtract(1, "day").format("YYYY-MM-DD"), ); const [exportParts, setExportParts] = useState>(DEFAULT_EXPORT_PARTS); const [exporting, setExporting] = useState(false); const [exportError, setExportError] = useState(null); const [inventoryPreview, setInventoryPreview] = useState(null); const [inventoryLoading, setInventoryLoading] = useState(false); const [inventoryRunning, setInventoryRunning] = useState(false); const [inventoryError, setInventoryError] = useState(null); const [inventoryMessage, setInventoryMessage] = useState(null); const adjRunInFlight = useRef(false); const [adjRunning, setAdjRunning] = useState(false); const [adjError, setAdjError] = useState(null); const [adjMessage, setAdjMessage] = useState(null); /** Default yesterday; freeze-night dump set to today so ADJ lands on dump day. */ const [adjDate, setAdjDate] = useState(() => dayjs().subtract(1, "day").format("YYYY-MM-DD"), ); const [invQuery, setInvQuery] = useState(""); const [lotQuery, setLotQuery] = useState(""); const [invHits, setInvHits] = useState([]); const [lotHits, setLotHits] = useState([]); const [searchError, setSearchError] = useState(null); const [searching, setSearching] = useState(false); const [scope, setScope] = useState(null); const [scopeLoading, setScopeLoading] = useState(false); const loadInventory = useCallback(async () => { if (inventoryLoadInFlight.current) return; inventoryLoadInFlight.current = true; setInventoryLoading(true); setInventoryError(null); try { const data = await fetchStockLedgerFixInventory(); setInventoryPreview(data); } catch (e) { console.error(e); setInventoryError("無法載入 inventory 1.0 預覽(需要 ADMIN / TESTING)"); setInventoryPreview(null); } finally { setInventoryLoading(false); inventoryLoadInFlight.current = false; } }, []); const loadDay = useCallback(async (date: string) => { if (detailInFlight.current) return; detailInFlight.current = true; setDetailLoading(true); setDetailError(null); setFixMessage(null); try { const data = await fetchStockLedgerFixDay(date); setDetail(data); } catch (e) { console.error(e); setDetailError("無法載入當日檢查"); setDetail(null); } finally { setDetailLoading(false); detailInFlight.current = false; } }, []); useEffect(() => { void loadInventory(); }, [loadInventory]); useEffect(() => { if (tab === "day" && selected) { void loadDay(selected.format("YYYY-MM-DD")); } }, [selected, loadDay, tab]); const onFixDay = async () => { if (!selected || fixInFlight.current) return; const date = selected.format("YYYY-MM-DD"); if (selected.isAfter(dayjs(), "day")) { setFixError("不能修未來日期"); return; } const picked = selectedDaySteps(daySteps); if (picked.length === 0) { setFixError("請至少勾一個步驟(預設全跑 2.1–2.6)"); return; } const steps = stepsPayload(daySteps); const stepLabel = steps?.join("、") ?? "全部 2.1–2.6"; if (steps) { const ok = window.confirm( `只跑 ${stepLabel}(${date})。未勾的步驟不會重算。確定?`, ); if (!ok) return; } fixInFlight.current = true; setFixing(true); setFixError(null); setFixMessage(null); try { const res = await runStockLedgerFixDay(date, steps); setFixMessage( `已修 ${res.date}(${stepLabel}):lot ${res.filledLotLineId}、uom ${res.filledUomId}、inventory ${res.filledInventoryId}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}${lotLineFixNote(res)}`, ); await loadDay(date); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "修復失敗")); } finally { setFixing(false); fixInFlight.current = false; } }; const onInventory = async () => { if (inventoryRunInFlight.current) return; const ok = window.confirm( "方案 A:保留現有 inventory.id;從 lot 回填 stockUomId(舊 uomId 是 stock 則改回 base);補缺 (itemId, stockUomId);用 lot 的 in−out 重算 onHand/unavailable。不寫 onHoldQty、不改 inventory_lot_line。確定執行?", ); if (!ok) return; inventoryRunInFlight.current = true; setInventoryRunning(true); setInventoryError(null); setInventoryMessage(null); try { const res = await runStockLedgerFixInventory(); setInventoryMessage( `inventory 1.0 完成:回填 stockUomId ${res.patchedStockUomId} 列、新增 ${res.inserted} 列、殘列 deleted ${res.orphansDeleted ?? 0}、重算 ${res.updated} 列、之後仍缺 UOM ${res.missingUomPairsAfter}、仍 NULL stockUomId ${res.nullStockUomIdAfter}`, ); await loadInventory(); } catch (e) { console.error(e); setInventoryError(apiErrorMessage(e, "inventory 1.0 失敗")); } finally { setInventoryRunning(false); inventoryRunInFlight.current = false; } }; const onAdjApply = async () => { if (adjRunInFlight.current) return; const d = adjDate.trim(); if (!d) { setAdjError("請填 ADJ 日期"); return; } if (d > dayjs().format("YYYY-MM-DD")) { setAdjError("ADJ 日期不能是未來"); return; } const ok = window.confirm( `會在 ${d} 對最後餘額不等於 onHandQty 的庫存桶各寫一筆 ADJ(入或出),掛在既有 lot 上,並寫入庫存調整記錄。不改 lot line、不改 inventory。已對齊的桶不會再寫;缺調整記錄的舊 ADJ 會補上。確定?`, ); if (!ok) return; adjRunInFlight.current = true; setAdjRunning(true); setAdjError(null); setAdjMessage(null); try { const res = await runStockLedgerFixAdj(d); setAdjMessage( `ADJ ${res.adjDate}:入 ${res.insertedIn}、出 ${res.insertedOut}、調整記錄 ${res.adjustmentRecords ?? 0}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}`, ); } catch (e) { console.error(e); setAdjError(apiErrorMessage(e, "ADJ 對齊失敗")); } finally { setAdjRunning(false); adjRunInFlight.current = false; } }; const onSearchInventory = async () => { if (searchInFlight.current || !invQuery.trim()) return; searchInFlight.current = true; setSearching(true); setSearchError(null); try { const hits = await searchStockLedgerFixInventory(invQuery.trim()); setInvHits(hits); setScope(null); } catch (e) { console.error(e); setSearchError(apiErrorMessage(e, "搜尋失敗")); } finally { setSearching(false); searchInFlight.current = false; } }; const onSearchLot = async () => { if (searchInFlight.current || !lotQuery.trim()) return; searchInFlight.current = true; setSearching(true); setSearchError(null); try { const hits = await searchStockLedgerFixLot(lotQuery.trim()); setLotHits(hits); setScope(null); } catch (e) { console.error(e); setSearchError(apiErrorMessage(e, "搜尋失敗")); } finally { setSearching(false); searchInFlight.current = false; } }; const loadInventoryScope = async (id: number) => { if (scopeInFlight.current) return; scopeInFlight.current = true; setScopeLoading(true); setFixError(null); setFixMessage(null); try { setScope(await fetchStockLedgerFixInventoryScope(id)); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "無法載入 inventory")); setScope(null); } finally { setScopeLoading(false); scopeInFlight.current = false; } }; const loadLotScope = async (id: number) => { if (scopeInFlight.current) return; scopeInFlight.current = true; setScopeLoading(true); setFixError(null); setFixMessage(null); try { setScope(await fetchStockLedgerFixLotScope(id)); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "無法載入 lot")); setScope(null); } finally { setScopeLoading(false); scopeInFlight.current = false; } }; const onFixInventoryScope = async () => { if (!scope || scope.kind !== "inventory" || fixInFlight.current) return; const ok = window.confirm( "會重算這顆 inventory 到今天為止全部 ledger 的 lotQty、日結,以及這顆料的 balance。確定?", ); if (!ok) return; fixInFlight.current = true; setFixing(true); setFixError(null); setFixMessage(null); try { const res = await runStockLedgerFixInventoryScope(scope.id); setFixMessage( `已修 inventory ${scope.id}:lot ${res.filledLotLineId}、uom ${res.filledUomId}、inventory ${res.filledInventoryId}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}${lotLineFixNote(res)}`, ); await loadInventoryScope(scope.id); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "修復失敗")); } finally { setFixing(false); fixInFlight.current = false; } }; const onFixLotScope = async () => { if (!scope || scope.kind !== "lot" || fixInFlight.current) return; const ok = window.confirm( "只修這張 lot 的 lotQty 與日結,不會改整顆料的 balance。確定?", ); if (!ok) return; fixInFlight.current = true; setFixing(true); setFixError(null); setFixMessage(null); try { const res = await runStockLedgerFixLotScope(scope.id); setFixMessage( `已修 lot ${scope.id}:lot ${res.filledLotLineId}、uom ${res.filledUomId}、inventory ${res.filledInventoryId}、lotQty ${res.filledLotQty}、日結 ${res.dayRowsWritten}${lotLineFixNote(res)}`, ); await loadLotScope(scope.id); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "修復失敗")); } finally { setFixing(false); fixInFlight.current = false; } }; const canFixDay = Boolean(selected && !selected.isAfter(dayjs(), "day")); const onFixDayRange = async () => { if (fixInFlight.current) return; const from = chainFrom.trim(); const to = chainTo.trim(); const today = dayjs().format("YYYY-MM-DD"); if (!from || !to) { setFixError("請填區間修 from / to"); return; } if (to < from) { setFixError("to 必須 ≥ from"); return; } if (from < FIRST_LEDGER_DAY.format("YYYY-MM-DD")) { setFixError(`from 不能早於 ${FIRST_LEDGER_DAY.format("YYYY-MM-DD")}`); return; } if (to > today) { setFixError("不能修未來日期(今天可以,給 freeze dump 夜修)"); return; } const picked = selectedDaySteps(daySteps); if (picked.length === 0) { setFixError("請至少勾一個步驟(預設全跑 2.1–2.6)"); return; } const steps = stepsPayload(daySteps); const stepLabel = steps?.join("、") ?? "全部 2.1–2.6"; const ok = window.confirm( `一次修 ${from} → ${to}(步驟:${stepLabel})。改為逐日跑(每天 commit)。中途失敗時已跑完的天會留下。確定?`, ); if (!ok) return; fixInFlight.current = true; setChainRunning(true); setFixing(true); setFixError(null); setFixMessage(null); setChainProgress(`區間修 ${from} → ${to}(${stepLabel})進行中…`); try { const res = await runStockLedgerFixRange(from, to, steps); setFixMessage( `區間修完成 ${res.date}(${stepLabel}):lot ${res.filledLotLineId}、uom ${res.filledUomId}、inventory ${res.filledInventoryId}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}${lotLineFixNote(res)}`, ); setSelected(dayjs(to)); setChainProgress(null); } catch (e) { console.error(e); setFixError(apiErrorMessage(e, "區間修失敗")); } finally { setFixing(false); setChainRunning(false); fixInFlight.current = false; } }; const onExportSql = async () => { if (exportInFlight.current) return; const from = exportFrom.trim(); const to = exportTo.trim(); if (!from || !to) { setExportError("請填 from / to"); return; } if (to < from) { setExportError("to 必須 ≥ from"); return; } const picked = selectedExportParts(exportParts); if (picked.length === 0) { setExportError("請至少勾一個匯出項目"); return; } if (exportParts["2.3"] && !exportParts["1.0"]) { const ok = window.confirm( "已勾 2.3 inventoryId 但未勾 1.0 inventory:正式庫若缺新 id,UPDATE 會指到空列。建議一併勾 1.0(完整檔)。仍要匯出?", ); if (!ok) return; } const parts = exportPartsPayload(exportParts); exportInFlight.current = true; setExporting(true); setExportError(null); try { await downloadStockLedgerFixSql(from, to, parts); } catch (e) { console.error(e); const data = (e as { response?: { data?: unknown } })?.response?.data; if (data instanceof Blob) { try { const text = (await data.text()).trim().slice(0, 400); setExportError(text || "匯出失敗"); } catch { setExportError(apiErrorMessage(e, "匯出失敗")); } } else { setExportError(apiErrorMessage(e, "匯出失敗")); } } finally { setExporting(false); exportInFlight.current = false; } }; return ( 1.0 inventory(方案 A) 保留現有 id;lot 有、inventory 沒有的 UOM 會新增;onHand = Σ(in−out) 按 item+UOM。沒有 lot 的列會變成 0。同一料多顆舊列時只留一顆 stock UOM 桶,其餘 deleted=1。請在進出倉暫停時執行,再逐日 Fix。 {inventoryError && {inventoryError}} {inventoryMessage && ( {inventoryMessage} )} {inventoryLoading && !inventoryPreview && } {inventoryPreview && ( 現有 inventory {inventoryPreview.inventoryRows} 列;lot 的 item+UOM {inventoryPreview.lotUomPairs} 組;缺{" "} {inventoryPreview.missingUomPairs} 組;stockUomId 仍 NULL{" "} {inventoryPreview.nullStockUomId} 列。 )} 2.7 ADJ:對齊最後餘額與 onHandQty 選 ADJ 日期後直接執行。預設昨天;freeze dump 夜可選今天。最後餘額低於 onHandQty 的桶補一筆入倉 ADJ,高於 onHandQty 的桶補一筆出倉 ADJ,掛在既有 lot 上。不改 lot line、不改 inventory。一次執行共用一張入倉單、一張出倉單;已對齊的桶再跑不會再寫。 { setAdjDate(e.target.value); setAdjMessage(null); }} disabled={adjRunning} InputLabelProps={{ shrink: true }} inputProps={{ max: dayjs().format("YYYY-MM-DD") }} /> {adjError && {adjError}} {adjMessage && {adjMessage}} {adjRunning && } { setTab(v); setFixError(null); setFixMessage(null); setSearchError(null); }} > {tab === "day" && ( setSelected(v)} views={["year", "month", "day"]} openTo="day" minDate={FIRST_LEDGER_DAY} maxDate={dayjs()} /> 點左上標題可先選年再選月。換月不會查資料;點某一天才檢查當天。今天可修(freeze dump 夜)。 {selected ? selected.format("YYYY-MM-DD") : "請選日期"} {detailError && {detailError}} {fixError && {fixError}} {fixMessage && {fixMessage}} {chainProgress && {chainProgress}} {detailLoading && } {detail && !detailLoading && ( <> 當天 ledger {detail.cnt} 列 )} 步驟(預設全跑) 套 SQL 後只改 inventoryId 時只勾 2.3。只跑部分時未勾的不會重算。 {DAY_FIX_STEPS.map((step) => ( setDaySteps((prev) => ({ ...prev, [step]: checked })) } /> } label={DAY_STEP_LABEL[step]} /> ))} 區間修(2.1–2.6 一次) 使用上面勾的步驟,一次 API 但改為逐日(與單日修相同 SQL,每天 commit)。可含今天(freeze dump 夜)。前端最長約 20 分鐘;大區間仍建議匯出 SQL。 setChainFrom(e.target.value)} disabled={chainRunning} InputLabelProps={{ shrink: true }} /> setChainTo(e.target.value)} disabled={chainRunning} InputLabelProps={{ shrink: true }} /> 匯出已修 SQL(新庫用) 勾選要匯出的段落(同日修步驟)。套檔順序:1.0 → 2.1 → ledger(±2.3)→ 2.7 入出倉單+調整記錄+ADJ → 2.6。完整檔且正式庫 freeze/id 同源、日期蓋過全部已修日子時,結果與已修庫相同。預設只匯 ledger+日結(舊行為)。 {EXPORT_PARTS.map((part) => ( setExportParts((prev) => ({ ...prev, [part]: checked })) } /> } label={EXPORT_PART_LABEL[part]} /> ))} {exportError && {exportError}} setExportFrom(e.target.value)} InputLabelProps={{ shrink: true }} /> setExportTo(e.target.value)} InputLabelProps={{ shrink: true }} /> )} {tab === "inventory" && ( 搜 itemCode 或 inventoryId。Fix 會重算這顆料到今天為止全部流水的 lotQty、日結與 balance,不必逐日點日曆。白天倉還在寫這顆料時,修完後新流水仍由 live writer 接。 setInvQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void onSearchInventory(); }} /> {searchError && {searchError}} {invHits.length > 0 && ( inventoryId itemCode uomId ledger 列 {invHits.map((h) => ( void loadInventoryScope(h.inventoryId)} sx={{ cursor: "pointer" }} > {h.inventoryId} {h.itemCode ?? "—"} {h.uomId ?? "—"} {h.ledgerCnt} ))}
)} {fixError && {fixError}} {fixMessage && {fixMessage}} {scopeLoading && } {scope?.kind === "inventory" && !scopeLoading && ( <> {scope.itemCode ?? "?"} / inventory {scope.id} / uom {scope.uomId ?? "?"} ;{scope.firstDate} → {scope.lastDate};{scope.cnt} 列;最後 balance {scope.lastBalance ?? "—"} )}
)} {tab === "lot" && ( 搜 lotNo 或 inventoryLotLineId。Fix 只重算這張 lot 的 lotQty 與日結,不改 balance。 setLotQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void onSearchLot(); }} /> {searchError && {searchError}} {lotHits.length > 0 && ( lot line id lotNo itemCode inventoryId ledger 列 {lotHits.map((h) => ( void loadLotScope(h.inventoryLotLineId)} sx={{ cursor: "pointer" }} > {h.inventoryLotLineId} {h.lotNo ?? "—"} {h.itemCode ?? "—"} {h.inventoryId ?? "—"} {h.ledgerCnt} ))}
)} {fixError && {fixError}} {fixMessage && {fixMessage}} {scopeLoading && } {scope?.kind === "lot" && !scopeLoading && ( <> {scope.lotNo ?? "?"} / lot {scope.id} / {scope.itemCode ?? "?"} ;{scope.firstDate} → {scope.lastDate};{scope.cnt} 列;最後 lotQtyAfter {scope.lastLotQtyAfter ?? "—"} )}
)}
); }; export default StockLedgerFixPageClient;