FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

1322 řádky
45 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useRef, useState } from "react";
  3. import {
  4. Alert,
  5. Box,
  6. Button,
  7. Checkbox,
  8. Chip,
  9. CircularProgress,
  10. FormControlLabel,
  11. Paper,
  12. Stack,
  13. Tab,
  14. Table,
  15. TableBody,
  16. TableCell,
  17. TableHead,
  18. TableRow,
  19. Tabs,
  20. TextField,
  21. Typography,
  22. } from "@mui/material";
  23. import { DateCalendar } from "@mui/x-date-pickers/DateCalendar";
  24. import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
  25. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  26. import { useTranslation } from "react-i18next";
  27. import dayjs, { type Dayjs } from "dayjs";
  28. import "dayjs/locale/zh-hk";
  29. import "dayjs/locale/en";
  30. import {
  31. fetchStockLedgerFixAdjPreview,
  32. runStockLedgerFixAdj,
  33. fetchStockLedgerFixDay,
  34. fetchStockLedgerFixInventory,
  35. fetchStockLedgerFixInventoryScope,
  36. fetchStockLedgerFixLotScope,
  37. runStockLedgerFixDay,
  38. runStockLedgerFixRange,
  39. runStockLedgerFixInventory,
  40. runStockLedgerFixInventoryScope,
  41. runStockLedgerFixLotScope,
  42. searchStockLedgerFixInventory,
  43. searchStockLedgerFixLot,
  44. downloadStockLedgerFixSql,
  45. type StockLedgerFixAdjPreview,
  46. type StockLedgerFixCheckPart,
  47. type StockLedgerFixDayDetail,
  48. type StockLedgerFixInventoryPreview,
  49. type StockLedgerFixScopeDetail,
  50. type StockLedgerFixSearchInventoryHit,
  51. type StockLedgerFixSearchLotHit,
  52. } from "@/app/api/stockLedgerFix/client";
  53. const FIRST_LEDGER_DAY = dayjs("2026-03-01");
  54. const DAY_FIX_STEPS = ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] as const;
  55. type DayFixStep = (typeof DAY_FIX_STEPS)[number];
  56. const ALL_DAY_STEPS: Record<DayFixStep, boolean> = {
  57. "2.1": true,
  58. "2.2": true,
  59. "2.3": true,
  60. "2.4": true,
  61. "2.5": true,
  62. "2.6": true,
  63. };
  64. const DAY_STEP_I18N: Record<DayFixStep, string> = {
  65. "2.1": "step21",
  66. "2.2": "step22",
  67. "2.3": "step23",
  68. "2.4": "step24",
  69. "2.5": "step25",
  70. "2.6": "step26",
  71. };
  72. /** Export SQL parts (aligned with fix steps + 1.0 / 2.7). */
  73. const EXPORT_PARTS = ["1.0", "2.3", "ledger", "2.6", "2.7"] as const;
  74. type ExportPart = (typeof EXPORT_PARTS)[number];
  75. const DEFAULT_EXPORT_PARTS: Record<ExportPart, boolean> = {
  76. "1.0": false,
  77. "2.3": false,
  78. ledger: true,
  79. "2.6": true,
  80. "2.7": false,
  81. };
  82. const FULL_EXPORT_PARTS: Record<ExportPart, boolean> = {
  83. "1.0": true,
  84. "2.3": true,
  85. ledger: true,
  86. "2.6": true,
  87. "2.7": true,
  88. };
  89. const EXPORT_PART_I18N: Record<ExportPart, string> = {
  90. "1.0": "export10",
  91. "2.3": "export23",
  92. ledger: "exportLedger",
  93. "2.6": "export26",
  94. "2.7": "export27",
  95. };
  96. function selectedExportParts(flags: Record<ExportPart, boolean>): ExportPart[] {
  97. return EXPORT_PARTS.filter((p) => flags[p]);
  98. }
  99. function exportPartsPayload(flags: Record<ExportPart, boolean>): string[] | undefined {
  100. const selected = selectedExportParts(flags);
  101. if (selected.length === 0) return undefined;
  102. // Always send explicit list so backend does not fall back to legacy default alone
  103. return selected;
  104. }
  105. function selectedDaySteps(flags: Record<DayFixStep, boolean>): DayFixStep[] {
  106. return DAY_FIX_STEPS.filter((s) => flags[s]);
  107. }
  108. function stepsPayload(flags: Record<DayFixStep, boolean>): string[] | undefined {
  109. const selected = selectedDaySteps(flags);
  110. if (selected.length === 0 || selected.length === DAY_FIX_STEPS.length) return undefined;
  111. return selected;
  112. }
  113. function apiErrorMessage(e: unknown, fallback: string): string {
  114. if (e && typeof e === "object" && "response" in e) {
  115. const data = (e as { response?: { data?: unknown } }).response?.data;
  116. if (typeof data === "string" && data.trim()) {
  117. return data.trim().slice(0, 400);
  118. }
  119. if (data && typeof data === "object") {
  120. const msg = (data as { message?: unknown }).message;
  121. if (typeof msg === "string" && msg.trim()) {
  122. return msg.trim().slice(0, 400);
  123. }
  124. }
  125. }
  126. if (e instanceof Error && e.message) return e.message;
  127. return fallback;
  128. }
  129. function partVerdict(
  130. part: StockLedgerFixCheckPart,
  131. ): "correct" | "miss" | "incorrect" | "over-issue" | "can-fix" | "cannot-fix" {
  132. if (part.group === "canFix") {
  133. return part.miss > 0 || part.incorrect > 0 ? "can-fix" : "correct";
  134. }
  135. if (part.group === "cannotFix") {
  136. return part.miss > 0 || part.incorrect > 0 ? "cannot-fix" : "correct";
  137. }
  138. if (part.key === "overIssue") {
  139. if (part.incorrect > 0 || part.miss > 0) return "over-issue";
  140. return "correct";
  141. }
  142. if (part.incorrect > 0) return "incorrect";
  143. if (part.miss > 0) return "miss";
  144. if (part.key === "dayTable" && part.ok === 0) return "miss";
  145. return "correct";
  146. }
  147. const VERDICT_LABEL: Record<
  148. ReturnType<typeof partVerdict>,
  149. "verdictCorrect" | "verdictMiss" | "verdictOverIssue" | "verdictCanFix" | "verdictCannotFix" | "verdictIncorrect"
  150. > = {
  151. correct: "verdictCorrect",
  152. miss: "verdictMiss",
  153. "over-issue": "verdictOverIssue",
  154. "can-fix": "verdictCanFix",
  155. "cannot-fix": "verdictCannotFix",
  156. incorrect: "verdictIncorrect",
  157. };
  158. function CheckPartsTable({ parts }: { parts: StockLedgerFixCheckPart[] }) {
  159. const { t } = useTranslation("stockLedgerFix");
  160. const field = parts.filter((p) => !p.group || p.group === "field");
  161. const canFix = parts.filter(
  162. (p) => p.group === "canFix" && p.miss + p.incorrect > 0,
  163. );
  164. const cannotFix = parts.filter(
  165. (p) => p.group === "cannotFix" && p.miss + p.incorrect > 0,
  166. );
  167. const renderRows = (rows: StockLedgerFixCheckPart[]) =>
  168. rows.map((p) => {
  169. const v = partVerdict(p);
  170. return (
  171. <TableRow key={p.key}>
  172. <TableCell sx={{ whiteSpace: "normal", wordBreak: "break-word" }}>
  173. {t(`part.${p.key}`, { defaultValue: p.label })}
  174. </TableCell>
  175. <TableCell>
  176. <Chip
  177. size="small"
  178. label={t(VERDICT_LABEL[v])}
  179. color={
  180. v === "correct"
  181. ? "success"
  182. : v === "miss" || v === "over-issue" || v === "can-fix"
  183. ? "warning"
  184. : "error"
  185. }
  186. />
  187. </TableCell>
  188. <TableCell align="right">{p.ok}</TableCell>
  189. <TableCell align="right">{p.miss}</TableCell>
  190. <TableCell align="right">{p.incorrect}</TableCell>
  191. </TableRow>
  192. );
  193. });
  194. return (
  195. <Stack spacing={2}>
  196. <Table size="small" sx={{ width: "100%" }}>
  197. <TableHead>
  198. <TableRow>
  199. <TableCell sx={{ whiteSpace: "normal" }}>{t("checkItem")}</TableCell>
  200. <TableCell>{t("checkStatus")}</TableCell>
  201. <TableCell align="right">{t("checkCorrect")}</TableCell>
  202. <TableCell align="right">{t("checkMiss")}</TableCell>
  203. <TableCell align="right">{t("checkIncorrect")}</TableCell>
  204. </TableRow>
  205. </TableHead>
  206. <TableBody>{renderRows(field)}</TableBody>
  207. </Table>
  208. {canFix.length > 0 && (
  209. <>
  210. <Typography variant="subtitle2" sx={{ whiteSpace: "normal" }}>
  211. {t("canAutoFix")}
  212. </Typography>
  213. <Table size="small">
  214. <TableHead>
  215. <TableRow>
  216. <TableCell>{t("checkReason")}</TableCell>
  217. <TableCell>{t("checkStatus")}</TableCell>
  218. <TableCell align="right">{t("checkCorrect")}</TableCell>
  219. <TableCell align="right">{t("checkRows")}</TableCell>
  220. <TableCell align="right">{t("checkDash")}</TableCell>
  221. </TableRow>
  222. </TableHead>
  223. <TableBody>{renderRows(canFix)}</TableBody>
  224. </Table>
  225. </>
  226. )}
  227. {cannotFix.length > 0 && (
  228. <>
  229. <Typography variant="subtitle2" sx={{ whiteSpace: "normal" }}>
  230. {t("cannotAutoFix")}
  231. </Typography>
  232. <Table size="small">
  233. <TableHead>
  234. <TableRow>
  235. <TableCell>{t("checkReason")}</TableCell>
  236. <TableCell>{t("checkStatus")}</TableCell>
  237. <TableCell align="right">{t("checkCorrect")}</TableCell>
  238. <TableCell align="right">{t("checkDash")}</TableCell>
  239. <TableCell align="right">{t("checkRows")}</TableCell>
  240. </TableRow>
  241. </TableHead>
  242. <TableBody>{renderRows(cannotFix)}</TableBody>
  243. </Table>
  244. </>
  245. )}
  246. </Stack>
  247. );
  248. }
  249. const StockLedgerFixPageClient: React.FC = () => {
  250. const { t, i18n } = useTranslation("stockLedgerFix");
  251. const isZh = (i18n.language || "zh").startsWith("zh");
  252. const listSep = isZh ? "、" : ", ";
  253. const [tab, setTab] = useState<"day" | "inventory" | "lot">("day");
  254. const [selected, setSelected] = useState<Dayjs | null>(() =>
  255. dayjs().subtract(1, "day"),
  256. );
  257. const [detail, setDetail] = useState<StockLedgerFixDayDetail | null>(null);
  258. const [detailLoading, setDetailLoading] = useState(false);
  259. const [detailError, setDetailError] = useState<string | null>(null);
  260. const [fixing, setFixing] = useState(false);
  261. const [fixError, setFixError] = useState<string | null>(null);
  262. const [fixMessage, setFixMessage] = useState<string | null>(null);
  263. const detailInFlight = useRef(false);
  264. const fixInFlight = useRef(false);
  265. const inventoryLoadInFlight = useRef(false);
  266. const inventoryRunInFlight = useRef(false);
  267. const searchInFlight = useRef(false);
  268. const scopeInFlight = useRef(false);
  269. const exportInFlight = useRef(false);
  270. const [chainFrom, setChainFrom] = useState("2026-03-15");
  271. const [chainTo, setChainTo] = useState(() =>
  272. dayjs().subtract(1, "day").format("YYYY-MM-DD"),
  273. );
  274. const [chainRunning, setChainRunning] = useState(false);
  275. const [chainProgress, setChainProgress] = useState<string | null>(null);
  276. const [daySteps, setDaySteps] = useState<Record<DayFixStep, boolean>>(ALL_DAY_STEPS);
  277. const [exportFrom, setExportFrom] = useState("2026-03-15");
  278. const [exportTo, setExportTo] = useState(() =>
  279. dayjs().subtract(1, "day").format("YYYY-MM-DD"),
  280. );
  281. const [exportParts, setExportParts] =
  282. useState<Record<ExportPart, boolean>>(DEFAULT_EXPORT_PARTS);
  283. const [exporting, setExporting] = useState(false);
  284. const [exportError, setExportError] = useState<string | null>(null);
  285. const [inventoryPreview, setInventoryPreview] =
  286. useState<StockLedgerFixInventoryPreview | null>(null);
  287. const [inventoryLoading, setInventoryLoading] = useState(false);
  288. const [inventoryRunning, setInventoryRunning] = useState(false);
  289. const [inventoryError, setInventoryError] = useState<string | null>(null);
  290. const [inventoryMessage, setInventoryMessage] = useState<string | null>(null);
  291. const adjLoadInFlight = useRef(false);
  292. const adjRunInFlight = useRef(false);
  293. const [adjPreview, setAdjPreview] = useState<StockLedgerFixAdjPreview | null>(null);
  294. const [adjLoading, setAdjLoading] = useState(false);
  295. const [adjRunning, setAdjRunning] = useState(false);
  296. const [adjError, setAdjError] = useState<string | null>(null);
  297. const [adjMessage, setAdjMessage] = useState<string | null>(null);
  298. /** Default yesterday; freeze-night dump set to today so ADJ lands on dump day. */
  299. const [adjDate, setAdjDate] = useState(() =>
  300. dayjs().subtract(1, "day").format("YYYY-MM-DD"),
  301. );
  302. const [invQuery, setInvQuery] = useState("");
  303. const [lotQuery, setLotQuery] = useState("");
  304. const [invHits, setInvHits] = useState<StockLedgerFixSearchInventoryHit[]>([]);
  305. const [lotHits, setLotHits] = useState<StockLedgerFixSearchLotHit[]>([]);
  306. const [searchError, setSearchError] = useState<string | null>(null);
  307. const [searching, setSearching] = useState(false);
  308. const [scope, setScope] = useState<StockLedgerFixScopeDetail | null>(null);
  309. const [scopeLoading, setScopeLoading] = useState(false);
  310. const loadInventory = useCallback(async () => {
  311. if (inventoryLoadInFlight.current) return;
  312. inventoryLoadInFlight.current = true;
  313. setInventoryLoading(true);
  314. setInventoryError(null);
  315. try {
  316. const data = await fetchStockLedgerFixInventory();
  317. setInventoryPreview(data);
  318. } catch (e) {
  319. console.error(e);
  320. setInventoryError(t("inventory10LoadError"));
  321. setInventoryPreview(null);
  322. } finally {
  323. setInventoryLoading(false);
  324. inventoryLoadInFlight.current = false;
  325. }
  326. }, [t]);
  327. const loadDay = useCallback(async (date: string) => {
  328. if (detailInFlight.current) return;
  329. detailInFlight.current = true;
  330. setDetailLoading(true);
  331. setDetailError(null);
  332. setFixMessage(null);
  333. try {
  334. const data = await fetchStockLedgerFixDay(date);
  335. setDetail(data);
  336. } catch (e) {
  337. console.error(e);
  338. setDetailError(t("dayLoadError"));
  339. setDetail(null);
  340. } finally {
  341. setDetailLoading(false);
  342. detailInFlight.current = false;
  343. }
  344. }, [t]);
  345. const loadAdjPreview = useCallback(async () => {
  346. if (adjLoadInFlight.current) return;
  347. const d = adjDate.trim();
  348. if (!d) {
  349. setAdjError(t("adjDateRequired"));
  350. return;
  351. }
  352. if (d > dayjs().format("YYYY-MM-DD")) {
  353. setAdjError(t("adjDateFuture"));
  354. return;
  355. }
  356. adjLoadInFlight.current = true;
  357. setAdjLoading(true);
  358. setAdjError(null);
  359. try {
  360. setAdjPreview(await fetchStockLedgerFixAdjPreview(d));
  361. } catch (e) {
  362. console.error(e);
  363. setAdjError(apiErrorMessage(e, t("adjLoadError")));
  364. setAdjPreview(null);
  365. } finally {
  366. setAdjLoading(false);
  367. adjLoadInFlight.current = false;
  368. }
  369. }, [adjDate, t]);
  370. useEffect(() => {
  371. void loadInventory();
  372. }, [loadInventory]);
  373. useEffect(() => {
  374. if (tab === "day" && selected) {
  375. void loadDay(selected.format("YYYY-MM-DD"));
  376. }
  377. }, [selected, loadDay, tab]);
  378. const onFixDay = async () => {
  379. if (!selected || fixInFlight.current) return;
  380. const date = selected.format("YYYY-MM-DD");
  381. if (selected.isAfter(dayjs(), "day")) {
  382. setFixError(t("cannotFixFuture"));
  383. return;
  384. }
  385. const picked = selectedDaySteps(daySteps);
  386. if (picked.length === 0) {
  387. setFixError(t("pickAtLeastOneStep"));
  388. return;
  389. }
  390. const steps = stepsPayload(daySteps);
  391. const stepLabel = steps?.join(listSep) ?? t("allSteps216");
  392. if (steps) {
  393. const ok = window.confirm(
  394. t("confirmPartialSteps", { steps: stepLabel, date }),
  395. );
  396. if (!ok) return;
  397. }
  398. fixInFlight.current = true;
  399. setFixing(true);
  400. setFixError(null);
  401. setFixMessage(null);
  402. try {
  403. const res = await runStockLedgerFixDay(date, steps);
  404. setFixMessage(
  405. t("fixDayDone", {
  406. date: res.date,
  407. steps: stepLabel,
  408. lot: res.filledLotLineId,
  409. uom: res.filledUomId,
  410. inventory: res.filledInventoryId,
  411. lotQty: res.filledLotQty,
  412. balance: res.filledBalance,
  413. dayRows: res.dayRowsWritten,
  414. }),
  415. );
  416. await loadDay(date);
  417. } catch (e) {
  418. console.error(e);
  419. setFixError(apiErrorMessage(e, t("fixFailed")));
  420. } finally {
  421. setFixing(false);
  422. fixInFlight.current = false;
  423. }
  424. };
  425. const onInventory = async () => {
  426. if (inventoryRunInFlight.current) return;
  427. const ok = window.confirm(t("inventory10Confirm"));
  428. if (!ok) return;
  429. inventoryRunInFlight.current = true;
  430. setInventoryRunning(true);
  431. setInventoryError(null);
  432. setInventoryMessage(null);
  433. try {
  434. const res = await runStockLedgerFixInventory();
  435. setInventoryMessage(
  436. t("inventory10Done", {
  437. patched: res.patchedStockUomId,
  438. inserted: res.inserted,
  439. orphans: res.orphansDeleted ?? 0,
  440. updated: res.updated,
  441. missingAfter: res.missingUomPairsAfter,
  442. nullAfter: res.nullStockUomIdAfter,
  443. }),
  444. );
  445. await loadInventory();
  446. } catch (e) {
  447. console.error(e);
  448. setInventoryError(apiErrorMessage(e, t("inventory10Fail")));
  449. } finally {
  450. setInventoryRunning(false);
  451. inventoryRunInFlight.current = false;
  452. }
  453. };
  454. const onAdjApply = async () => {
  455. if (adjRunInFlight.current) return;
  456. const d = adjDate.trim() || adjPreview?.adjDate;
  457. if (!d) {
  458. setAdjError(t("adjDateAndPreviewRequired"));
  459. return;
  460. }
  461. const ok = window.confirm(
  462. t("adjConfirm", {
  463. date: d,
  464. overIssueCount: adjPreview?.overIssueCount ?? 0,
  465. sumOverIssue: adjPreview?.sumOverIssue ?? "?",
  466. adjInCount: adjPreview?.adjInCount ?? 0,
  467. adjOutCount: adjPreview?.adjOutCount ?? 0,
  468. sumMissIn: adjPreview?.sumMissIn ?? "?",
  469. sumMissOut: adjPreview?.sumMissOut ?? "?",
  470. }),
  471. );
  472. if (!ok) return;
  473. adjRunInFlight.current = true;
  474. setAdjRunning(true);
  475. setAdjError(null);
  476. setAdjMessage(null);
  477. try {
  478. const res = await runStockLedgerFixAdj(d);
  479. setAdjMessage(
  480. t("adjDone", {
  481. date: res.adjDate,
  482. overIssuePatched: res.overIssuePatched,
  483. insertedIn: res.insertedIn,
  484. insertedOut: res.insertedOut,
  485. filledLotQty: res.filledLotQty,
  486. filledBalance: res.filledBalance,
  487. dayRowsWritten: res.dayRowsWritten,
  488. }),
  489. );
  490. await loadAdjPreview();
  491. } catch (e) {
  492. console.error(e);
  493. setAdjError(apiErrorMessage(e, t("adjFail")));
  494. } finally {
  495. setAdjRunning(false);
  496. adjRunInFlight.current = false;
  497. }
  498. };
  499. const onSearchInventory = async () => {
  500. if (searchInFlight.current || !invQuery.trim()) return;
  501. searchInFlight.current = true;
  502. setSearching(true);
  503. setSearchError(null);
  504. try {
  505. const hits = await searchStockLedgerFixInventory(invQuery.trim());
  506. setInvHits(hits);
  507. setScope(null);
  508. } catch (e) {
  509. console.error(e);
  510. setSearchError(apiErrorMessage(e, t("searchFailed")));
  511. } finally {
  512. setSearching(false);
  513. searchInFlight.current = false;
  514. }
  515. };
  516. const onSearchLot = async () => {
  517. if (searchInFlight.current || !lotQuery.trim()) return;
  518. searchInFlight.current = true;
  519. setSearching(true);
  520. setSearchError(null);
  521. try {
  522. const hits = await searchStockLedgerFixLot(lotQuery.trim());
  523. setLotHits(hits);
  524. setScope(null);
  525. } catch (e) {
  526. console.error(e);
  527. setSearchError(apiErrorMessage(e, t("searchFailed")));
  528. } finally {
  529. setSearching(false);
  530. searchInFlight.current = false;
  531. }
  532. };
  533. const loadInventoryScope = async (id: number) => {
  534. if (scopeInFlight.current) return;
  535. scopeInFlight.current = true;
  536. setScopeLoading(true);
  537. setFixError(null);
  538. setFixMessage(null);
  539. try {
  540. setScope(await fetchStockLedgerFixInventoryScope(id));
  541. } catch (e) {
  542. console.error(e);
  543. setFixError(apiErrorMessage(e, t("invLoadError")));
  544. setScope(null);
  545. } finally {
  546. setScopeLoading(false);
  547. scopeInFlight.current = false;
  548. }
  549. };
  550. const loadLotScope = async (id: number) => {
  551. if (scopeInFlight.current) return;
  552. scopeInFlight.current = true;
  553. setScopeLoading(true);
  554. setFixError(null);
  555. setFixMessage(null);
  556. try {
  557. setScope(await fetchStockLedgerFixLotScope(id));
  558. } catch (e) {
  559. console.error(e);
  560. setFixError(apiErrorMessage(e, t("lotLoadError")));
  561. setScope(null);
  562. } finally {
  563. setScopeLoading(false);
  564. scopeInFlight.current = false;
  565. }
  566. };
  567. const onFixInventoryScope = async () => {
  568. if (!scope || scope.kind !== "inventory" || fixInFlight.current) return;
  569. const ok = window.confirm(t("invFixConfirm"));
  570. if (!ok) return;
  571. fixInFlight.current = true;
  572. setFixing(true);
  573. setFixError(null);
  574. setFixMessage(null);
  575. try {
  576. const res = await runStockLedgerFixInventoryScope(scope.id);
  577. setFixMessage(
  578. t("invFixDone", {
  579. id: scope.id,
  580. lot: res.filledLotLineId,
  581. uom: res.filledUomId,
  582. inventory: res.filledInventoryId,
  583. lotQty: res.filledLotQty,
  584. balance: res.filledBalance,
  585. dayRows: res.dayRowsWritten,
  586. }),
  587. );
  588. await loadInventoryScope(scope.id);
  589. } catch (e) {
  590. console.error(e);
  591. setFixError(apiErrorMessage(e, t("fixFailed")));
  592. } finally {
  593. setFixing(false);
  594. fixInFlight.current = false;
  595. }
  596. };
  597. const onFixLotScope = async () => {
  598. if (!scope || scope.kind !== "lot" || fixInFlight.current) return;
  599. const ok = window.confirm(t("lotFixConfirm"));
  600. if (!ok) return;
  601. fixInFlight.current = true;
  602. setFixing(true);
  603. setFixError(null);
  604. setFixMessage(null);
  605. try {
  606. const res = await runStockLedgerFixLotScope(scope.id);
  607. setFixMessage(
  608. t("lotFixDone", {
  609. id: scope.id,
  610. lot: res.filledLotLineId,
  611. uom: res.filledUomId,
  612. inventory: res.filledInventoryId,
  613. lotQty: res.filledLotQty,
  614. dayRows: res.dayRowsWritten,
  615. }),
  616. );
  617. await loadLotScope(scope.id);
  618. } catch (e) {
  619. console.error(e);
  620. setFixError(apiErrorMessage(e, t("fixFailed")));
  621. } finally {
  622. setFixing(false);
  623. fixInFlight.current = false;
  624. }
  625. };
  626. const canFixDay = Boolean(selected && !selected.isAfter(dayjs(), "day"));
  627. const onFixDayRange = async () => {
  628. if (fixInFlight.current) return;
  629. const from = chainFrom.trim();
  630. const to = chainTo.trim();
  631. const today = dayjs().format("YYYY-MM-DD");
  632. if (!from || !to) {
  633. setFixError(t("rangeFromRequired"));
  634. return;
  635. }
  636. if (to < from) {
  637. setFixError(t("toMustBeGteFrom"));
  638. return;
  639. }
  640. if (from < FIRST_LEDGER_DAY.format("YYYY-MM-DD")) {
  641. setFixError(t("fromTooEarly", { date: FIRST_LEDGER_DAY.format("YYYY-MM-DD") }));
  642. return;
  643. }
  644. if (to > today) {
  645. setFixError(t("cannotFixFutureRange"));
  646. return;
  647. }
  648. const picked = selectedDaySteps(daySteps);
  649. if (picked.length === 0) {
  650. setFixError(t("pickAtLeastOneStep"));
  651. return;
  652. }
  653. const steps = stepsPayload(daySteps);
  654. const stepLabel = steps?.join(listSep) ?? t("allSteps216");
  655. const ok = window.confirm(
  656. t("rangeConfirm", { from, to, steps: stepLabel }),
  657. );
  658. if (!ok) return;
  659. fixInFlight.current = true;
  660. setChainRunning(true);
  661. setFixing(true);
  662. setFixError(null);
  663. setFixMessage(null);
  664. setChainProgress(t("rangeProgress", { from, to, steps: stepLabel }));
  665. try {
  666. const res = await runStockLedgerFixRange(from, to, steps);
  667. setFixMessage(
  668. t("rangeDone", {
  669. date: res.date,
  670. steps: stepLabel,
  671. lot: res.filledLotLineId,
  672. uom: res.filledUomId,
  673. inventory: res.filledInventoryId,
  674. lotQty: res.filledLotQty,
  675. balance: res.filledBalance,
  676. dayRows: res.dayRowsWritten,
  677. }),
  678. );
  679. setSelected(dayjs(to));
  680. setChainProgress(null);
  681. } catch (e) {
  682. console.error(e);
  683. setFixError(apiErrorMessage(e, t("rangeFail")));
  684. } finally {
  685. setFixing(false);
  686. setChainRunning(false);
  687. fixInFlight.current = false;
  688. }
  689. };
  690. const onExportSql = async () => {
  691. if (exportInFlight.current) return;
  692. const from = exportFrom.trim();
  693. const to = exportTo.trim();
  694. if (!from || !to) {
  695. setExportError(t("exportFromToRequired"));
  696. return;
  697. }
  698. if (to < from) {
  699. setExportError(t("toMustBeGteFrom"));
  700. return;
  701. }
  702. const picked = selectedExportParts(exportParts);
  703. if (picked.length === 0) {
  704. setExportError(t("exportPickAtLeastOne"));
  705. return;
  706. }
  707. if (exportParts["2.3"] && !exportParts["1.0"]) {
  708. const ok = window.confirm(t("export23Without10"));
  709. if (!ok) return;
  710. }
  711. const parts = exportPartsPayload(exportParts);
  712. exportInFlight.current = true;
  713. setExporting(true);
  714. setExportError(null);
  715. try {
  716. await downloadStockLedgerFixSql(from, to, parts);
  717. } catch (e) {
  718. console.error(e);
  719. const data = (e as { response?: { data?: unknown } })?.response?.data;
  720. if (data instanceof Blob) {
  721. try {
  722. const text = (await data.text()).trim().slice(0, 400);
  723. setExportError(text || t("exportFail"));
  724. } catch {
  725. setExportError(apiErrorMessage(e, t("exportFail")));
  726. }
  727. } else {
  728. setExportError(apiErrorMessage(e, t("exportFail")));
  729. }
  730. } finally {
  731. setExporting(false);
  732. exportInFlight.current = false;
  733. }
  734. };
  735. return (
  736. <Stack spacing={3}>
  737. <Paper sx={{ p: 2 }}>
  738. <Stack spacing={1.5}>
  739. <Typography variant="h6">{t("inventory10Title")}</Typography>
  740. <Typography variant="body2" color="text.secondary">
  741. {t("inventory10Description")}
  742. </Typography>
  743. {inventoryError && <Alert severity="error">{inventoryError}</Alert>}
  744. {inventoryMessage && (
  745. <Alert severity="success">{inventoryMessage}</Alert>
  746. )}
  747. {inventoryLoading && !inventoryPreview && <CircularProgress size={24} />}
  748. {inventoryPreview && (
  749. <Typography variant="body2">
  750. {t("inventory10Preview", {
  751. rows: inventoryPreview.inventoryRows,
  752. lotPairs: inventoryPreview.lotUomPairs,
  753. missing: inventoryPreview.missingUomPairs,
  754. nullUom: inventoryPreview.nullStockUomId,
  755. })}
  756. </Typography>
  757. )}
  758. <Box>
  759. <Button
  760. variant="contained"
  761. color="warning"
  762. disabled={inventoryRunning || inventoryLoading}
  763. onClick={() => void onInventory()}
  764. >
  765. {inventoryRunning ? t("inventory10Running") : t("inventory10Run")}
  766. </Button>
  767. </Box>
  768. </Stack>
  769. </Paper>
  770. <Paper sx={{ p: 2 }}>
  771. <Stack spacing={1.5}>
  772. <Typography variant="h6">{t("adjTitle")}</Typography>
  773. <Typography variant="body2" color="text.secondary">
  774. {t("adjDescription")}
  775. </Typography>
  776. <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
  777. <TextField
  778. size="small"
  779. type="date"
  780. label={t("adjDateLabel")}
  781. value={adjDate}
  782. onChange={(e) => {
  783. setAdjDate(e.target.value);
  784. setAdjPreview(null);
  785. setAdjMessage(null);
  786. }}
  787. disabled={adjLoading || adjRunning}
  788. InputLabelProps={{ shrink: true }}
  789. inputProps={{ max: dayjs().format("YYYY-MM-DD") }}
  790. />
  791. <Button
  792. size="small"
  793. disabled={adjLoading || adjRunning}
  794. onClick={() => {
  795. setAdjDate(dayjs().format("YYYY-MM-DD"));
  796. setAdjPreview(null);
  797. }}
  798. >
  799. {t("adjTodayFreeze")}
  800. </Button>
  801. <Button
  802. size="small"
  803. disabled={adjLoading || adjRunning}
  804. onClick={() => {
  805. setAdjDate(dayjs().subtract(1, "day").format("YYYY-MM-DD"));
  806. setAdjPreview(null);
  807. }}
  808. >
  809. {t("adjYesterday")}
  810. </Button>
  811. </Stack>
  812. {adjError && <Alert severity="error">{adjError}</Alert>}
  813. {adjMessage && <Alert severity="success">{adjMessage}</Alert>}
  814. {adjLoading && !adjPreview && <CircularProgress size={24} />}
  815. {adjPreview && (
  816. <Typography variant="body2">
  817. {t("adjPreviewSummary", {
  818. date: adjPreview.adjDate,
  819. lotCount: adjPreview.lotCount,
  820. overIssueCount: adjPreview.overIssueCount,
  821. sumOverIssue: adjPreview.sumOverIssue,
  822. adjInCount: adjPreview.adjInCount,
  823. adjOutCount: adjPreview.adjOutCount,
  824. sumMissIn: adjPreview.sumMissIn,
  825. sumMissOut: adjPreview.sumMissOut,
  826. skuNet: adjPreview.skuNet,
  827. skuNetNote:
  828. adjPreview.skuNet !== "0" ? t("adjSkuNetNote") : "",
  829. revNote:
  830. adjPreview.skippedNegCount > 0
  831. ? t("adjRevNote", { count: adjPreview.skippedNegCount })
  832. : "",
  833. })}
  834. </Typography>
  835. )}
  836. {adjPreview && adjPreview.rows.length > 0 && (
  837. <Table size="small">
  838. <TableHead>
  839. <TableRow>
  840. <TableCell>{t("adjColLotLineId")}</TableCell>
  841. <TableCell>{t("adjColItemCode")}</TableCell>
  842. <TableCell align="right">{t("adjColLineInOut")}</TableCell>
  843. <TableCell align="right">{t("adjColLedgerInOut")}</TableCell>
  844. <TableCell align="right">{t("adjColMissIn")}</TableCell>
  845. <TableCell align="right">{t("adjColMissOut")}</TableCell>
  846. <TableCell align="right">{t("adjColOverIssue")}</TableCell>
  847. </TableRow>
  848. </TableHead>
  849. <TableBody>
  850. {adjPreview.rows.map((r) => (
  851. <TableRow key={r.lotLineId}>
  852. <TableCell>{r.lotLineId}</TableCell>
  853. <TableCell>{r.itemCode ?? t("checkDash")}</TableCell>
  854. <TableCell align="right">
  855. {r.lineIn} / {r.lineOut}
  856. </TableCell>
  857. <TableCell align="right">
  858. {r.ledgerIn} / {r.ledgerOut}
  859. </TableCell>
  860. <TableCell align="right">{r.missIn}</TableCell>
  861. <TableCell align="right">{r.missOut}</TableCell>
  862. <TableCell align="right">{r.overIssue}</TableCell>
  863. </TableRow>
  864. ))}
  865. </TableBody>
  866. </Table>
  867. )}
  868. <Stack direction="row" spacing={1}>
  869. <Button
  870. variant="outlined"
  871. disabled={adjLoading || adjRunning}
  872. onClick={() => void loadAdjPreview()}
  873. >
  874. {adjLoading
  875. ? t("adjPreviewing")
  876. : adjPreview
  877. ? t("adjPreviewAgain")
  878. : t("adjPreview")}
  879. </Button>
  880. <Button
  881. variant="contained"
  882. color="warning"
  883. disabled={adjRunning || adjLoading || !adjPreview}
  884. onClick={() => void onAdjApply()}
  885. >
  886. {adjRunning ? t("adjApplying") : t("adjApply")}
  887. </Button>
  888. </Stack>
  889. </Stack>
  890. </Paper>
  891. <Paper sx={{ px: 2, pt: 1 }}>
  892. <Tabs
  893. value={tab}
  894. onChange={(_, v: "day" | "inventory" | "lot") => {
  895. setTab(v);
  896. setFixError(null);
  897. setFixMessage(null);
  898. setSearchError(null);
  899. }}
  900. >
  901. <Tab label={t("tabCalendar")} value="day" />
  902. <Tab label={t("tabInventory")} value="inventory" />
  903. <Tab label={t("tabLot")} value="lot" />
  904. </Tabs>
  905. </Paper>
  906. {tab === "day" && (
  907. <Stack
  908. direction={{ xs: "column", md: "row" }}
  909. spacing={3}
  910. alignItems="flex-start"
  911. sx={{ width: "100%", minWidth: 0 }}
  912. >
  913. <Paper sx={{ p: 1, maxWidth: 360, width: "100%", flex: "0 0 auto" }}>
  914. <LocalizationProvider
  915. dateAdapter={AdapterDayjs}
  916. adapterLocale={isZh ? "zh-hk" : "en"}
  917. >
  918. <DateCalendar
  919. value={selected}
  920. onChange={(v) => setSelected(v)}
  921. views={["year", "month", "day"]}
  922. openTo="day"
  923. minDate={FIRST_LEDGER_DAY}
  924. maxDate={dayjs()}
  925. />
  926. </LocalizationProvider>
  927. <Typography
  928. variant="caption"
  929. color="text.secondary"
  930. sx={{ px: 2, pb: 1, display: "block", whiteSpace: "pre-line" }}
  931. >
  932. {t("calendarHint")}
  933. </Typography>
  934. </Paper>
  935. <Paper sx={{ p: 2, flex: 1, minWidth: 0, maxWidth: "100%", width: { xs: "100%", md: "auto" } }}>
  936. <Stack spacing={2}>
  937. <Typography variant="h6">
  938. {selected ? selected.format("YYYY-MM-DD") : t("selectDate")}
  939. </Typography>
  940. {detailError && <Alert severity="error">{detailError}</Alert>}
  941. {fixError && <Alert severity="error">{fixError}</Alert>}
  942. {fixMessage && <Alert severity="success">{fixMessage}</Alert>}
  943. {chainProgress && <Alert severity="info">{chainProgress}</Alert>}
  944. {detailLoading && <CircularProgress size={24} />}
  945. {detail && !detailLoading && (
  946. <>
  947. <Typography variant="body2" color="text.secondary">
  948. {t("dayLedgerCount", { cnt: detail.cnt })}
  949. </Typography>
  950. <CheckPartsTable parts={detail.parts} />
  951. </>
  952. )}
  953. <Box>
  954. <Typography variant="subtitle2" sx={{ mb: 0.5 }}>
  955. {t("stepsTitle")}
  956. </Typography>
  957. <Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
  958. {t("stepsHint")}
  959. </Typography>
  960. <Stack direction="row" spacing={0} flexWrap="wrap" useFlexGap>
  961. {DAY_FIX_STEPS.map((step) => (
  962. <FormControlLabel
  963. key={step}
  964. sx={{
  965. mr: 1,
  966. "& .MuiFormControlLabel-label": { whiteSpace: "normal" },
  967. }}
  968. control={
  969. <Checkbox
  970. size="small"
  971. checked={daySteps[step]}
  972. disabled={fixing || chainRunning}
  973. onChange={(_, checked) =>
  974. setDaySteps((prev) => ({ ...prev, [step]: checked }))
  975. }
  976. />
  977. }
  978. label={t(DAY_STEP_I18N[step])}
  979. />
  980. ))}
  981. <Button
  982. size="small"
  983. disabled={fixing || chainRunning}
  984. onClick={() => setDaySteps(ALL_DAY_STEPS)}
  985. >
  986. {t("selectAll")}
  987. </Button>
  988. </Stack>
  989. </Box>
  990. <Box>
  991. <Button
  992. variant="contained"
  993. disabled={!canFixDay || fixing || chainRunning}
  994. onClick={() => void onFixDay()}
  995. >
  996. {fixing && !chainRunning ? t("fixing") : t("fixThisDay")}
  997. </Button>
  998. </Box>
  999. <Box sx={{ pt: 1 }}>
  1000. <Typography variant="subtitle2" sx={{ mb: 1 }}>
  1001. {t("rangeTitle")}
  1002. </Typography>
  1003. <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
  1004. {t("rangeHint")}
  1005. </Typography>
  1006. <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
  1007. <TextField
  1008. size="small"
  1009. type="date"
  1010. label={t("from")}
  1011. value={chainFrom}
  1012. onChange={(e) => setChainFrom(e.target.value)}
  1013. disabled={chainRunning}
  1014. InputLabelProps={{ shrink: true }}
  1015. />
  1016. <TextField
  1017. size="small"
  1018. type="date"
  1019. label={t("to")}
  1020. value={chainTo}
  1021. onChange={(e) => setChainTo(e.target.value)}
  1022. disabled={chainRunning}
  1023. InputLabelProps={{ shrink: true }}
  1024. />
  1025. <Button
  1026. variant="contained"
  1027. disabled={fixing || chainRunning}
  1028. onClick={() => void onFixDayRange()}
  1029. >
  1030. {chainRunning ? t("rangeRunning") : t("rangeRun")}
  1031. </Button>
  1032. </Stack>
  1033. </Box>
  1034. <Box sx={{ pt: 1 }}>
  1035. <Typography variant="subtitle2" sx={{ mb: 1 }}>
  1036. {t("exportTitle")}
  1037. </Typography>
  1038. <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
  1039. {t("exportHint")}
  1040. </Typography>
  1041. <Stack direction="row" spacing={0} flexWrap="wrap" useFlexGap sx={{ mb: 1 }}>
  1042. {EXPORT_PARTS.map((part) => (
  1043. <FormControlLabel
  1044. key={part}
  1045. sx={{
  1046. mr: 1,
  1047. maxWidth: "100%",
  1048. "& .MuiFormControlLabel-label": { whiteSpace: "normal" },
  1049. }}
  1050. control={
  1051. <Checkbox
  1052. size="small"
  1053. checked={exportParts[part]}
  1054. disabled={exporting}
  1055. onChange={(_, checked) =>
  1056. setExportParts((prev) => ({ ...prev, [part]: checked }))
  1057. }
  1058. />
  1059. }
  1060. label={t(EXPORT_PART_I18N[part])}
  1061. />
  1062. ))}
  1063. <Button
  1064. size="small"
  1065. disabled={exporting}
  1066. onClick={() => setExportParts(DEFAULT_EXPORT_PARTS)}
  1067. >
  1068. {t("exportDefault")}
  1069. </Button>
  1070. <Button
  1071. size="small"
  1072. disabled={exporting}
  1073. onClick={() => setExportParts(FULL_EXPORT_PARTS)}
  1074. >
  1075. {t("exportFull")}
  1076. </Button>
  1077. </Stack>
  1078. {exportError && <Alert severity="error">{exportError}</Alert>}
  1079. <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
  1080. <TextField
  1081. size="small"
  1082. type="date"
  1083. label={t("from")}
  1084. value={exportFrom}
  1085. onChange={(e) => setExportFrom(e.target.value)}
  1086. InputLabelProps={{ shrink: true }}
  1087. />
  1088. <TextField
  1089. size="small"
  1090. type="date"
  1091. label={t("to")}
  1092. value={exportTo}
  1093. onChange={(e) => setExportTo(e.target.value)}
  1094. InputLabelProps={{ shrink: true }}
  1095. />
  1096. <Button
  1097. variant="outlined"
  1098. disabled={exporting}
  1099. onClick={() => void onExportSql()}
  1100. >
  1101. {exporting ? t("exporting") : t("exportSql")}
  1102. </Button>
  1103. </Stack>
  1104. </Box>
  1105. </Stack>
  1106. </Paper>
  1107. </Stack>
  1108. )}
  1109. {tab === "inventory" && (
  1110. <Paper sx={{ p: 2 }}>
  1111. <Stack spacing={2}>
  1112. <Typography variant="body2" color="text.secondary">
  1113. {t("invTabHint")}
  1114. </Typography>
  1115. <Stack direction="row" spacing={1}>
  1116. <TextField
  1117. size="small"
  1118. label={t("invSearchLabel")}
  1119. value={invQuery}
  1120. onChange={(e) => setInvQuery(e.target.value)}
  1121. onKeyDown={(e) => {
  1122. if (e.key === "Enter") void onSearchInventory();
  1123. }}
  1124. />
  1125. <Button
  1126. variant="outlined"
  1127. disabled={searching || !invQuery.trim()}
  1128. onClick={() => void onSearchInventory()}
  1129. >
  1130. {searching ? t("searching") : t("search")}
  1131. </Button>
  1132. </Stack>
  1133. {searchError && <Alert severity="error">{searchError}</Alert>}
  1134. {invHits.length > 0 && (
  1135. <Table size="small">
  1136. <TableHead>
  1137. <TableRow>
  1138. <TableCell>{t("colInventoryId")}</TableCell>
  1139. <TableCell>{t("colItemCode")}</TableCell>
  1140. <TableCell>{t("colUomId")}</TableCell>
  1141. <TableCell align="right">{t("colLedgerRows")}</TableCell>
  1142. </TableRow>
  1143. </TableHead>
  1144. <TableBody>
  1145. {invHits.map((h) => (
  1146. <TableRow
  1147. key={h.inventoryId}
  1148. hover
  1149. selected={scope?.kind === "inventory" && scope.id === h.inventoryId}
  1150. onClick={() => void loadInventoryScope(h.inventoryId)}
  1151. sx={{ cursor: "pointer" }}
  1152. >
  1153. <TableCell>{h.inventoryId}</TableCell>
  1154. <TableCell>{h.itemCode ?? t("checkDash")}</TableCell>
  1155. <TableCell>{h.uomId ?? t("checkDash")}</TableCell>
  1156. <TableCell align="right">{h.ledgerCnt}</TableCell>
  1157. </TableRow>
  1158. ))}
  1159. </TableBody>
  1160. </Table>
  1161. )}
  1162. {fixError && <Alert severity="error">{fixError}</Alert>}
  1163. {fixMessage && <Alert severity="success">{fixMessage}</Alert>}
  1164. {scopeLoading && <CircularProgress size={24} />}
  1165. {scope?.kind === "inventory" && !scopeLoading && (
  1166. <>
  1167. <Typography variant="body2">
  1168. {t("invScopeSummary", {
  1169. itemCode: scope.itemCode ?? "?",
  1170. id: scope.id,
  1171. uomId: scope.uomId ?? "?",
  1172. firstDate: scope.firstDate,
  1173. lastDate: scope.lastDate,
  1174. cnt: scope.cnt,
  1175. lastBalance: scope.lastBalance ?? t("checkDash"),
  1176. })}
  1177. </Typography>
  1178. <CheckPartsTable parts={scope.parts} />
  1179. <Box>
  1180. <Button
  1181. variant="contained"
  1182. disabled={fixing || scope.cnt === 0}
  1183. onClick={() => void onFixInventoryScope()}
  1184. >
  1185. {fixing ? t("fixing") : t("fixThisInventory")}
  1186. </Button>
  1187. </Box>
  1188. </>
  1189. )}
  1190. </Stack>
  1191. </Paper>
  1192. )}
  1193. {tab === "lot" && (
  1194. <Paper sx={{ p: 2 }}>
  1195. <Stack spacing={2}>
  1196. <Typography variant="body2" color="text.secondary">
  1197. {t("lotTabHint")}
  1198. </Typography>
  1199. <Stack direction="row" spacing={1}>
  1200. <TextField
  1201. size="small"
  1202. label={t("lotSearchLabel")}
  1203. value={lotQuery}
  1204. onChange={(e) => setLotQuery(e.target.value)}
  1205. onKeyDown={(e) => {
  1206. if (e.key === "Enter") void onSearchLot();
  1207. }}
  1208. />
  1209. <Button
  1210. variant="outlined"
  1211. disabled={searching || !lotQuery.trim()}
  1212. onClick={() => void onSearchLot()}
  1213. >
  1214. {searching ? t("searching") : t("search")}
  1215. </Button>
  1216. </Stack>
  1217. {searchError && <Alert severity="error">{searchError}</Alert>}
  1218. {lotHits.length > 0 && (
  1219. <Table size="small">
  1220. <TableHead>
  1221. <TableRow>
  1222. <TableCell>{t("colLotLineId")}</TableCell>
  1223. <TableCell>{t("colLotNo")}</TableCell>
  1224. <TableCell>{t("colItemCode")}</TableCell>
  1225. <TableCell>{t("colInventoryId")}</TableCell>
  1226. <TableCell align="right">{t("colLedgerRows")}</TableCell>
  1227. </TableRow>
  1228. </TableHead>
  1229. <TableBody>
  1230. {lotHits.map((h) => (
  1231. <TableRow
  1232. key={h.inventoryLotLineId}
  1233. hover
  1234. selected={scope?.kind === "lot" && scope.id === h.inventoryLotLineId}
  1235. onClick={() => void loadLotScope(h.inventoryLotLineId)}
  1236. sx={{ cursor: "pointer" }}
  1237. >
  1238. <TableCell>{h.inventoryLotLineId}</TableCell>
  1239. <TableCell>{h.lotNo ?? t("checkDash")}</TableCell>
  1240. <TableCell>{h.itemCode ?? t("checkDash")}</TableCell>
  1241. <TableCell>{h.inventoryId ?? t("checkDash")}</TableCell>
  1242. <TableCell align="right">{h.ledgerCnt}</TableCell>
  1243. </TableRow>
  1244. ))}
  1245. </TableBody>
  1246. </Table>
  1247. )}
  1248. {fixError && <Alert severity="error">{fixError}</Alert>}
  1249. {fixMessage && <Alert severity="success">{fixMessage}</Alert>}
  1250. {scopeLoading && <CircularProgress size={24} />}
  1251. {scope?.kind === "lot" && !scopeLoading && (
  1252. <>
  1253. <Typography variant="body2">
  1254. {t("lotScopeSummary", {
  1255. lotNo: scope.lotNo ?? "?",
  1256. id: scope.id,
  1257. itemCode: scope.itemCode ?? "?",
  1258. firstDate: scope.firstDate,
  1259. lastDate: scope.lastDate,
  1260. cnt: scope.cnt,
  1261. lastLotQtyAfter: scope.lastLotQtyAfter ?? t("checkDash"),
  1262. })}
  1263. </Typography>
  1264. <CheckPartsTable parts={scope.parts} />
  1265. <Box>
  1266. <Button
  1267. variant="contained"
  1268. disabled={fixing || scope.cnt === 0}
  1269. onClick={() => void onFixLotScope()}
  1270. >
  1271. {fixing ? t("fixing") : t("fixThisLot")}
  1272. </Button>
  1273. </Box>
  1274. </>
  1275. )}
  1276. </Stack>
  1277. </Paper>
  1278. )}
  1279. </Stack>
  1280. );
  1281. };
  1282. export default StockLedgerFixPageClient;