Pārlūkot izejas kodu

Merge branch 'do_workbench_fix' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend into production

# Conflicts:
#	src/app/api/inventory/actions.ts
#	src/app/api/inventory/index.ts
#	src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
#	src/components/InventorySearch/InventoryLotLineTable.tsx
#	src/components/InventorySearch/InventorySearch.tsx
production
Harry Groves pirms 5 stundām
vecāks
revīzija
0b0174e030
37 mainītis faili ar 3612 papildinājumiem un 474 dzēšanām
  1. +24
    -0
      src/app/(main)/settings/stockLedgerFix/page.tsx
  2. +0
    -2
      src/app/api/doworkbench/actions.ts
  3. +4
    -0
      src/app/api/inventory/index.ts
  4. +82
    -0
      src/app/api/inventory/inventoryBucket.ts
  5. +1
    -0
      src/app/api/jo/actions.ts
  6. +2
    -0
      src/app/api/jo/index.ts
  7. +0
    -2
      src/app/api/pickOrder/actions.ts
  8. +3
    -0
      src/app/api/settings/item/index.ts
  9. +1
    -0
      src/app/api/stockAdjustment/actions.ts
  10. +319
    -0
      src/app/api/stockLedgerFix/client.ts
  11. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  12. +25
    -9
      src/components/DoWorkbench/DoWorkbenchTabs.tsx
  13. +16
    -9
      src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx
  14. +26
    -16
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  15. +48
    -44
      src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx
  16. +901
    -179
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
  17. +4
    -0
      src/components/InventorySearch/InventoryLotLineTable.tsx
  18. +3
    -0
      src/components/InventorySearch/InventorySearch.tsx
  19. +9
    -9
      src/components/JoSave/JoRelease.tsx
  20. +9
    -9
      src/components/JoSave/PickTable.tsx
  21. +9
    -9
      src/components/JoSearch/JoSearch.tsx
  22. +9
    -9
      src/components/JoWorkbench/JoWorkbenchSearch.tsx
  23. +4
    -4
      src/components/PickOrderSearch/WorkbenchPickExecution.tsx
  24. +10
    -5
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  25. +1321
    -0
      src/components/StockLedgerFix/StockLedgerFixPageClient.tsx
  26. +59
    -32
      src/components/charts/SafeApexCharts.tsx
  27. +110
    -110
      src/i18n/en/common.json
  28. +43
    -1
      src/i18n/en/doWorkbench.json
  29. +2
    -0
      src/i18n/en/navigation.json
  30. +103
    -9
      src/i18n/en/pickOrder.json
  31. +147
    -0
      src/i18n/en/stockLedgerFix.json
  32. +3
    -3
      src/i18n/en/ticketReleaseTable.json
  33. +38
    -1
      src/i18n/zh/doWorkbench.json
  34. +2
    -0
      src/i18n/zh/navigation.json
  35. +99
    -2
      src/i18n/zh/pickOrder.json
  36. +147
    -0
      src/i18n/zh/stockLedgerFix.json
  37. +28
    -10
      src/utils/workbenchPickLotUtils.ts

+ 24
- 0
src/app/(main)/settings/stockLedgerFix/page.tsx Parādīt failu

@@ -0,0 +1,24 @@
import { Metadata } from "next";
import PageTitleBar from "@/components/PageTitleBar";
import StockLedgerFixPageClient from "@/components/StockLedgerFix/StockLedgerFixPageClient";
import { getServerI18n, I18nProvider } from "@/i18n";

export async function generateMetadata(): Promise<Metadata> {
const { t } = await getServerI18n("stockLedgerFix");
return { title: t("pageTitle") };
}

const StockLedgerFixPage: React.FC = async () => {
const { t } = await getServerI18n("stockLedgerFix");

return (
<>
<PageTitleBar title={t("pageTitle")} className="mb-4" />
<I18nProvider namespaces={["stockLedgerFix", "navigation", "common"]}>
<StockLedgerFixPageClient />
</I18nProvider>
</>
);
};

export default StockLedgerFixPage;

+ 0
- 2
src/app/api/doworkbench/actions.ts Parādīt failu

@@ -216,7 +216,6 @@ export async function fetchWorkbenchStoreLaneSummary(
return serverFetchJson<StoreLaneSummary>(url, {
method: "GET",
cache: "no-store",
next: { revalidate: 0 },
});
}

@@ -235,7 +234,6 @@ export async function fetchWorkbenchEtraLaneSummary(
const data = await serverFetchJson<WorkbenchEtraShopLaneGroup[]>(url, {
method: "GET",
cache: "no-store",
next: { revalidate: 0 },
});
return Array.isArray(data) ? data : [];
}


+ 4
- 0
src/app/api/inventory/index.ts Parādīt failu

@@ -14,6 +14,10 @@ export interface InventoryResult {
onHoldQty: number;
unavailableQty: number;
availableQty: number;
/** Inventory bucket row. Optional; absent on the one-row-per-item search page. */
stockUomId?: number | null;
stockUomCode?: string | null;
uomId?: number;
uomCode: string;
uomUdfudesc: string;
uomShortDesc: string;


+ 82
- 0
src/app/api/inventory/inventoryBucket.ts Parādīt failu

@@ -0,0 +1,82 @@
/** Keys used to pick one inventory row when an item has multiple stock-UOM buckets. */
export type InventoryBucketPick = {
itemId?: number | null;
itemCode?: string | null;
itemName?: string | null;
uomId?: number | null;
stockUomId?: number | null;
uom?: string | null;
shortUom?: string | null;
stockUom?: string | null;
};

export type InventoryBucketRow = {
itemId?: number | null;
itemCode?: string | null;
itemName?: string | null;
stockUomId?: number | null;
uomCode?: string | null;
uomUdfudesc?: string | null;
uomShortDesc?: string | null;
availableQty?: number | null;
onHandQty?: number | null;
unavailableQty?: number | null;
};

const norm = (s?: string | null) => (s ?? "").trim().toLowerCase();

/** Available = onHand − unavailable. Do not treat 0 as missing (`||` is wrong). */
export function inventoryAvailableQty(inv: InventoryBucketRow): number {
if (inv.availableQty != null && !Number.isNaN(Number(inv.availableQty))) {
return Number(inv.availableQty);
}
return Number(inv.onHandQty ?? 0) - Number(inv.unavailableQty ?? 0);
}

function itemMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean {
if (pick.itemId != null && inv.itemId != null) {
return Number(inv.itemId) === Number(pick.itemId);
}
if (pick.itemCode && inv.itemCode) {
return inv.itemCode === pick.itemCode;
}
if (pick.itemName && inv.itemName) {
return inv.itemName === pick.itemName;
}
return false;
}

function uomMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean {
const pickUomId = pick.stockUomId ?? pick.uomId;
if (pickUomId != null && inv.stockUomId != null) {
return Number(inv.stockUomId) === Number(pickUomId);
}
const labels = [pick.uom, pick.shortUom, pick.stockUom].map(norm).filter(Boolean);
if (labels.length === 0) return true;
const invLabels = [inv.uomUdfudesc, inv.uomShortDesc, inv.uomCode].map(norm);
return labels.some((l) => invLabels.includes(l));
}

export function matchInventoryBucket(
inventories: InventoryBucketRow[],
pick: InventoryBucketPick,
): InventoryBucketRow | undefined {
const itemHits = inventories.filter((inv) => itemMatches(inv, pick));
if (itemHits.length === 0) return undefined;
const hasUomPick =
pick.stockUomId != null ||
pick.uomId != null ||
[pick.uom, pick.shortUom, pick.stockUom].some((s) => Boolean(norm(s)));
if (hasUomPick) {
return itemHits.find((inv) => uomMatches(inv, pick));
}
return itemHits[0];
}

export function getStockAvailableFromInventories(
inventories: InventoryBucketRow[],
pick: InventoryBucketPick,
): number {
const inv = matchInventoryBucket(inventories, pick);
return inv ? inventoryAvailableQty(inv) : 0;
}

+ 1
- 0
src/app/api/jo/actions.ts Parādīt failu

@@ -533,6 +533,7 @@ export interface JobOrderLineInfo {

stockUom: string,
stockBaseUom: string,
stockUomId?: number | null,
availableStatus: string,
bomProcessId: number,


+ 2
- 0
src/app/api/jo/index.ts Parādīt failu

@@ -67,6 +67,8 @@ export interface JoDetail {

export interface JoDetailPickLine {
id: number;
itemId?: number;
uomId?: number;
code: string;
name: string;
type: string;


+ 0
- 2
src/app/api/pickOrder/actions.ts Parādīt failu

@@ -632,7 +632,6 @@ export async function fetchStoreLaneSummary(storeId: string, requiredDate?: stri
const response = await serverFetchJson<StoreLaneSummary>(url, {
method: "GET",
cache: "no-store",
next: { revalidate: 0 },
});
console.timeEnd(label);
return response;
@@ -856,7 +855,6 @@ export const fetchFGPickOrdersByUserIdWorkbench = async (userId: number) => {
method: "GET",
// Must be fresh: determines whether shell shows Floor/Lane panel or Detail.
cache: "no-store",
next: { revalidate: 0 },
},
);
};


+ 3
- 0
src/app/api/settings/item/index.ts Parādīt failu

@@ -66,6 +66,9 @@ export type ItemsResult = {
latestMarketUnitPrice?: number;
latestMupUpdatedDate?: string;
purchaseUnit?: string;
uomId?: number;
uom?: string;
uomDesc?: string;
};

export type Result = {


+ 1
- 0
src/app/api/stockAdjustment/actions.ts Parādīt failu

@@ -16,6 +16,7 @@ export interface StockAdjustmentLineRequest {
expiryDate: string;
warehouseId: number;
uom?: string | null;
uomId?: number | null;
remarks?: string | null;
}



+ 319
- 0
src/app/api/stockLedgerFix/client.ts Parādīt failu

@@ -0,0 +1,319 @@
"use client";

import axiosInstance from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

export type StockLedgerFixDayStatus = {
date: string;
cnt: number;
missLot: number;
missUom: number;
missInventoryId: number;
missLotQty: number;
dayTableLots: number;
};

export type StockLedgerFixCalendarResponse = {
from: string;
to: string;
days: StockLedgerFixDayStatus[];
};

export type StockLedgerFixCheckPart = {
key: string;
label: string;
ok: number;
miss: number;
incorrect: number;
group?: string;
};

export type StockLedgerFixDayDetail = {
date: string;
cnt: number;
parts: StockLedgerFixCheckPart[];
};

export type StockLedgerFixRunResponse = {
date: string;
filledLotLineId: number;
filledUomId: number;
filledInventoryId: number;
filledLotQty: number;
filledBalance: number;
dayRowsWritten: number;
stillMissLot: number;
stillMissUom: number;
stillMissInventoryId: number;
stillMissLotQty: number;
};

export type StockLedgerFixInventoryPreview = {
inventoryRows: number;
lotUomPairs: number;
missingUomPairs: number;
nullStockUomId: number;
};

export type StockLedgerFixInventoryResponse = {
inserted: number;
updated: number;
missingUomPairsAfter: number;
patchedStockUomId: number;
nullStockUomIdAfter: number;
orphansDeleted?: number;
};

export type StockLedgerFixSearchInventoryHit = {
inventoryId: number;
itemId: number | null;
itemCode: string | null;
uomId: number | null;
ledgerCnt: number;
};

export type StockLedgerFixSearchLotHit = {
inventoryLotLineId: number;
lotNo: string | null;
itemCode: string | null;
inventoryId: number | null;
ledgerCnt: number;
};

export type StockLedgerFixScopeDetail = {
kind: string;
id: number;
itemCode: string | null;
lotNo: string | null;
inventoryId: number | null;
uomId: number | null;
firstDate: string | null;
lastDate: string | null;
cnt: number;
lastBalance: string | null;
lastLotQtyAfter: string | null;
parts: StockLedgerFixCheckPart[];
};

export type StockLedgerFixAdjRow = {
lotLineId: number;
inventoryId: number | null;
itemCode: string | null;
lineIn: string;
lineOut: string;
ledgerIn: string;
ledgerOut: string;
missIn: string;
missOut: string;
overIssue: string;
};

export type StockLedgerFixAdjPreview = {
adjDate: string;
lotCount: number;
adjInCount: number;
adjOutCount: number;
skippedNegCount: number;
sumMissIn: string;
sumMissOut: string;
skuNet: string;
overIssueCount: number;
sumOverIssue: string;
rows: StockLedgerFixAdjRow[];
};

export type StockLedgerFixAdjResponse = {
adjDate: string;
insertedIn: number;
insertedOut: number;
overIssuePatched: number;
filledLotQty: number;
filledBalance: number;
dayRowsWritten: number;
};

export async function fetchStockLedgerFixAdjPreview(
adjDate?: string,
): Promise<StockLedgerFixAdjPreview> {
const response = await axiosInstance.get<StockLedgerFixAdjPreview>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/adj`,
{
params: {
rowLimit: 20,
...(adjDate ? { adjDate } : {}),
},
timeout: 600000,
},
);
return response.data;
}

export async function runStockLedgerFixAdj(
adjDate?: string,
): Promise<StockLedgerFixAdjResponse> {
const response = await axiosInstance.post<StockLedgerFixAdjResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/adj`,
adjDate ? { adjDate } : {},
{ timeout: 1200000 },
);
return response.data;
}

export async function fetchStockLedgerFixCalendar(
from: string,
to: string,
): Promise<StockLedgerFixCalendarResponse> {
const response = await axiosInstance.get<StockLedgerFixCalendarResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/calendar`,
{ params: { from, to } },
);
return response.data;
}

export async function fetchStockLedgerFixDay(
date: string,
): Promise<StockLedgerFixDayDetail> {
const response = await axiosInstance.get<StockLedgerFixDayDetail>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/day`,
{ params: { date } },
);
return response.data;
}

export async function runStockLedgerFixDay(
date: string,
steps?: string[],
): Promise<StockLedgerFixRunResponse> {
const response = await axiosInstance.post<StockLedgerFixRunResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/run`,
{ date, ...(steps && steps.length > 0 ? { steps } : {}) },
);
return response.data;
}

export async function runStockLedgerFixRange(
from: string,
to: string,
steps?: string[],
): Promise<StockLedgerFixRunResponse> {
const response = await axiosInstance.post<StockLedgerFixRunResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/run`,
{
mode: "range",
from,
to,
...(steps && steps.length > 0 ? { steps } : {}),
},
{ timeout: 1200000 },
);
return response.data;
}

export async function fetchStockLedgerFixInventory(): Promise<StockLedgerFixInventoryPreview> {
const response = await axiosInstance.get<StockLedgerFixInventoryPreview>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/inventory`,
);
return response.data;
}

export async function runStockLedgerFixInventory(): Promise<StockLedgerFixInventoryResponse> {
const response = await axiosInstance.post<StockLedgerFixInventoryResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/inventory`,
);
return response.data;
}

export async function searchStockLedgerFixInventory(
q: string,
): Promise<StockLedgerFixSearchInventoryHit[]> {
const response = await axiosInstance.get<StockLedgerFixSearchInventoryHit[]>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/lookup/inventory`,
{ params: { q } },
);
return response.data;
}

export async function searchStockLedgerFixLot(
q: string,
): Promise<StockLedgerFixSearchLotHit[]> {
const response = await axiosInstance.get<StockLedgerFixSearchLotHit[]>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/lookup/lot`,
{ params: { q } },
);
return response.data;
}

export async function fetchStockLedgerFixInventoryScope(
id: number,
): Promise<StockLedgerFixScopeDetail> {
const response = await axiosInstance.get<StockLedgerFixScopeDetail>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/scope/inventory`,
{ params: { id } },
);
return response.data;
}

export async function fetchStockLedgerFixLotScope(
id: number,
): Promise<StockLedgerFixScopeDetail> {
const response = await axiosInstance.get<StockLedgerFixScopeDetail>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/scope/lot`,
{ params: { id } },
);
return response.data;
}

export async function runStockLedgerFixInventoryScope(
inventoryId: number,
): Promise<StockLedgerFixRunResponse> {
const response = await axiosInstance.post<StockLedgerFixRunResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/run`,
{ mode: "inventory", inventoryId },
);
return response.data;
}

export async function runStockLedgerFixLotScope(
lotLineId: number,
): Promise<StockLedgerFixRunResponse> {
const response = await axiosInstance.post<StockLedgerFixRunResponse>(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/run`,
{ mode: "lot", lotLineId },
);
return response.data;
}

export async function downloadStockLedgerFixSql(
from: string,
to: string,
parts?: string[],
): Promise<void> {
const response = await axiosInstance.get(
`${NEXT_PUBLIC_API_URL}/stock-ledger-fix/export`,
{
params: {
from,
to,
...(parts && parts.length > 0 ? { parts } : {}),
},
paramsSerializer: {
indexes: null,
},
responseType: "blob",
timeout: 600000,
},
);
const blob = new Blob([response.data], { type: "application/sql;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const partTag =
parts && parts.length > 0 && parts.length < 5
? `_${parts.join("-").replaceAll(".", "")}`
: "";
a.download = `stock_ledger_fix_${from.replaceAll("-", "")}_${to.replaceAll("-", "")}${partTag}.sql`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}

+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Parādīt failu

@@ -35,6 +35,7 @@ const pathToLabelKey: { [path: string]: string } = {
"/settings/qrCodeHandle": "nav.breadcrumb.qrCodeHandle",
"/settings/deliveryOrderFloor": "nav.breadcrumb.deliveryOrderFloor",
"/settings/masterDataIssues": "nav.breadcrumb.masterDataIssues",
"/settings/stockLedgerFix": "nav.breadcrumb.stockLedgerFix",
"/settings/rss": "nav.breadcrumb.demandForecast",
"/settings/equipment": "nav.breadcrumb.equipment",
"/settings/equipment/MaintenanceEdit": "nav.breadcrumb.equipmentMaintenanceEdit",


+ 25
- 9
src/components/DoWorkbench/DoWorkbenchTabs.tsx Parādīt failu

@@ -186,7 +186,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom

const confirmResult = await Swal.fire({
title: t("Batch Print"),
text: `${t("Confirm print: (")}${releasedOrders.length}${t("piece(s))")}`,
text: t("Confirm print drafts", { count: releasedOrders.length }),
icon: "question",
showCancelButton: true,
confirmButtonText: t("Confirm"),
@@ -276,18 +276,29 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom
<Tabs
value={tab}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
sx={{
width: "100%",
maxWidth: "100%",
minHeight: 48,
borderBottom: 1,
borderColor: "divider",
"& .MuiTabs-flexContainer": {
columnGap: 2,
rowGap: 1,
columnGap: 0.5,
},
/* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */
"& .MuiTab-root": {
overflow: "visible",
minWidth: "auto",
px: 2,
minHeight: 48,
minWidth: 72,
maxWidth: "none",
px: 1.5,
py: 0.75,
whiteSpace: "nowrap",
lineHeight: 1.2,
textAlign: "center",
},
}}
>
@@ -297,7 +308,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom
sx={{
overflow: "visible",
/* 徽章在標籤右側外凸,預留空間避免與下一個 Tab 貼死 */
pr: etraIncompleteDopoCount > 99 ? 5 : etraIncompleteDopoCount > 0 ? 4 : 2,
pr: etraIncompleteDopoCount > 99 ? 2.5 : etraIncompleteDopoCount > 0 ? 2 : 1,
}}
label={
<Tooltip
@@ -329,7 +340,12 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom
<Typography
component="span"
variant="inherit"
sx={{ pr: etraIncompleteDopoCount > 0 ? 1 : 0 }}
sx={{
pr: etraIncompleteDopoCount > 0 ? 1 : 0,
whiteSpace: "nowrap",
lineHeight: 1.2,
textAlign: "center",
}}
>
{t("Etra Pick Order Detail")}
</Typography>
@@ -341,8 +357,8 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCom
<Tab label={t("Finished Good Record")} value={2} />
<Tab label={t("Finished Good Record (All)")} value={3} />
<Tab label={t("Ticket Release Table")} value={4} />
<Tab label={t("成品出倉出箱數量")} value={5} />
<Tab label={t("送貨路線摘要")} value={6} />
<Tab label={t("FG Carton Qty")} value={5} />
<Tab label={t("Truck Routing Summary")} value={6} />
</Tabs>

<TabPanel value={tab} index={0}>


+ 16
- 9
src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx Parādīt failu

@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import { useTranslation } from "react-i18next";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import {
@@ -18,6 +19,7 @@ import {
} from "@/lib/featureUsageLog";

const TruckRoutingSummaryTabWorkbench: React.FC = () => {
const { t } = useTranslation();
const [storeOptions, setStoreOptions] = useState<WorkbenchReportOption[]>([]);
const [laneOptions, setLaneOptions] = useState<WorkbenchReportOption[]>([]);
const [storeId, setStoreId] = useState("");
@@ -45,6 +47,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {

const canDownload = storeId && truckLanceCode && date && !loading;

const displayOptionLabel = (label: string) =>
String(label).trim() === "車線-X" ? t("Truck X") : label;

const onDownload = async () => {
if (!canDownload) return;
try {
@@ -55,7 +60,9 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {
});
if (precheck.hasUnpickedOrders) {
const confirmed = window.confirm(
`此車線仍有 ${precheck.unpickedOrderCount} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?`
t("Unpicked orders confirm download", {
count: precheck.unpickedOrderCount,
}),
);
if (!confirmed) return;
}
@@ -92,7 +99,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {
);
} catch (error) {
console.error("Failed to download Workbench Truck Routing Summary", error);
alert("下載 Workbench 送貨路線摘要失敗,請稍後再試。");
alert(t("Failed to download Workbench truck routing summary. Please try again later."));
} finally {
setLoading(false);
}
@@ -101,39 +108,39 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {
return (
<Box sx={{ maxWidth: 820 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
送貨路線摘要 (Workbench)
{t("Truck Routing Summary (Workbench)")}
</Typography>
<Stack direction={{ xs: "column", md: "row" }} spacing={2} sx={{ mb: 2 }}>
<TextField
select
fullWidth
label="2/F 或 4/F"
label={t("2/F or 4/F")}
value={storeId}
onChange={(e) => onStoreChange(e.target.value)}
>
{storeOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
{displayOptionLabel(opt.label)}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
label="車線"
label={t("Lane")}
value={truckLanceCode}
onChange={(e) => setTruckLanceCode(e.target.value)}
disabled={!storeId}
>
{laneOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
{displayOptionLabel(opt.label)}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="日期"
label={t("Date")}
type="date"
value={date}
InputLabelProps={{ shrink: true }}
@@ -146,7 +153,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {
disabled={!canDownload}
onClick={onDownload}
>
{loading ? "生成中..." : "下載報告 (PDF)"}
{loading ? t("Generating...") : t("Download report (PDF)")}
</Button>
</Box>
);


+ 26
- 16
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Parādīt failu

@@ -421,22 +421,30 @@ function isWorkbenchSourceLotExpired(lot: any): boolean {
return false;
}

function getWorkbenchSourceLotStatusSummary(lot: any): {
type PickOrderT = (key: string, options?: Record<string, unknown>) => string;

function getWorkbenchSourceLotStatusSummary(lot: any, t: PickOrderT): {
severity: "success" | "warning" | "error";
text: string;
} {
if (!lot) {
return { severity: "warning", text: "無法判斷此批號狀態" };
return { severity: "warning", text: t("Cannot determine this lot status") };
}
if (isWorkbenchSourceLotExpired(lot)) {
return { severity: "error", text: "此批號狀態:已過期" };
return { severity: "error", text: t("Lot status: expired") };
}
const solSt = String(lot.stockOutLineStatus || "").toLowerCase();
if (solSt === "rejected") {
return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" };
return {
severity: "warning",
text: t("This pick line was rejected. Please scan another lot."),
};
}
if (solSt === "completed" || solSt === "partially_completed") {
return { severity: "warning", text: "此出庫行:已完成,無需再提貨" };
return {
severity: "warning",
text: t("This pick line is already completed. No further pick needed."),
};
}
/**
* 無批次列:後端仍標 insufficient_stock,語意是「尚無可出庫批號」而非「已用畢」。
@@ -449,28 +457,28 @@ function getWorkbenchSourceLotStatusSummary(lot: any): {
if (isNoLotRow) {
return {
severity: "warning",
text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR",
text: t(
"No lot bound / no available stock line. Please scan nearby inbound or transfer QR.",
),
};
}
const av = String(lot.lotAvailability || "").toLowerCase();
if (av === "insufficient_stock") {
return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" };
return { severity: "warning", text: t("Lot status: depleted (no remaining stock)") };
}
const avail = Number(lot.availableQty);
if (lot.lotNo && Number.isFinite(avail) && avail <= 0) {
return { severity: "warning", text: "此批號狀態:已用畢(可用量為 0)" };
return { severity: "warning", text: t("Lot status: depleted (available qty is 0)") };
}
if (isInventoryLotLineUnavailable(lot)) {
return {
severity: "warning",
text: "此批號狀態:庫存不可用(未上架或行狀態不可用)",
text: t("Lot status: unavailable (not put away or line unavailable)"),
};
}
return { severity: "success", text: "此批號狀態:可提貨" };
return { severity: "success", text: t("Lot status: ready to pick") };
}

type PickOrderT = (key: string, options?: Record<string, unknown>) => string;

function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string {
const msg = raw.trim();
if (!msg) return msg;
@@ -1334,9 +1342,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
severity: undefined as "success" | "warning" | "error" | undefined,
};
}
const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot);
const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot, t);
return { text: s.text, severity: s.severity };
}, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot]);
}, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, t]);

const workbenchLotLabelSubmitQty = useMemo(() => {
if (!workbenchLotLabelContextLot) return 0;
@@ -1811,7 +1819,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
`此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
t("This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.", {
lot: scannedLot.lotNo || scannedStockInLineId,
}),
);
});
// Mark this SOL as processed to prevent re-processing
@@ -1864,7 +1874,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg("当前订单中没有此物品的批次信息");
setQrScanErrorMsg(t("No lot information for this item in the current order"));
});
return;
}


+ 48
- 44
src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx Parādīt failu

@@ -37,6 +37,7 @@ import {
printWorkbenchLotLabel,
} from "@/app/api/doworkbench/actions";
import { QRCodeSVG } from "qrcode.react";
import { useTranslation } from "react-i18next";

type ScanPayload = {
itemId: number;
@@ -167,6 +168,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
submitQty = null,
onSubmitQtyChange,
}) => {
const { t } = useTranslation();
const scanInputRef = useRef<HTMLInputElement | null>(null);
const [scanInput, setScanInput] = useState("");
const [scanError, setScanError] = useState<string | null>(null);
@@ -210,8 +212,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
useEffect(() => {
if (!open) return;
resetAll();
const t = setTimeout(() => scanInputRef.current?.focus(), 50);
return () => clearTimeout(t);
const focusTimer = setTimeout(() => scanInputRef.current?.focus(), 50);
return () => clearTimeout(focusTimer);
}, [open, resetAll]);

const loadPrinters = useCallback(async () => {
@@ -224,13 +226,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
setPrinters([]);
setSnackbar({
open: true,
message: e instanceof Error ? e.message : "載入印表機清單失敗",
message: e instanceof Error ? e.message : t("Failed to load printer list"),
severity: "error",
});
} finally {
setPrintersLoading(false);
}
}, []);
}, [t]);

useEffect(() => {
if (!open) return;
@@ -283,23 +285,23 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
setAnalysis(data);
setSnackbar({
open: true,
message: "已載入同品可用批號清單",
message: t("Loaded available lots for this item"),
severity: "success",
});
} catch (e) {
setAnalysis(null);
setScanError(e instanceof Error ? e.message : "分析失敗");
setScanError(e instanceof Error ? e.message : t("Analysis failed"));
} finally {
setAnalysisLoading(false);
}
},
[resolveExpectedUomId],
[resolveExpectedUomId, t],
);

const analyzeByItem = useCallback(
async (itemId: number) => {
if (!Number.isFinite(itemId) || itemId <= 0) {
setScanError("無效 itemId,無法載入批號清單。");
setScanError(t("Invalid itemId, cannot load lot list."));
return;
}
setLastItemId(itemId);
@@ -325,17 +327,17 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
});
setSnackbar({
open: true,
message: "已載入同品可用批號清單",
message: t("Loaded available lots for this item"),
severity: "success",
});
} catch (e) {
setAnalysis(null);
setScanError(e instanceof Error ? e.message : "分析失敗");
setScanError(e instanceof Error ? e.message : t("Analysis failed"));
} finally {
setAnalysisLoading(false);
}
},
[resolveExpectedUomId],
[resolveExpectedUomId, t],
);

const handleAnalyze = useCallback(async () => {
@@ -343,13 +345,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
const payload = safeParseScanPayload(raw);
if (!payload) {
setScanError(
'掃碼內容格式錯誤,請重新掃碼',
t("Invalid scan format. Please scan again."),
);
setAnalysis(null);
return;
}
await analyzePayload(payload);
}, [scanInput, analyzePayload]);
}, [scanInput, analyzePayload, t]);

const handleRefreshLots = useCallback(async () => {
const payload = lastPayload ?? safeParseScanPayload(scanInput.trim());
@@ -368,12 +370,12 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
if (!payload) {
setSnackbar({
open: true,
message: "請先掃碼或查詢一次,才可刷新批號清單。",
message: t("Scan or look up once before refreshing the lot list."),
severity: "info",
});
return;
}
}, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput]);
}, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput, t]);

useEffect(() => {
if (!open) return;
@@ -470,7 +472,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
if (selectedPrinterId === "") {
setSnackbar({
open: true,
message: "請先選擇印表機",
message: t("Please select a printer first"),
severity: "error",
});
return;
@@ -478,7 +480,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
if (printQty < 1 || !Number.isFinite(printQty)) {
setSnackbar({
open: true,
message: "列印張數需為大於等於 1 的整數",
message: t("Print quantity must be an integer of 1 or more"),
severity: "error",
});
return;
@@ -493,25 +495,25 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
});
setSnackbar({
open: true,
message: `已送出列印:Lot ${lotNo}`,
message: t("Print sent: Lot {{lotNo}}", { lotNo }),
severity: "success",
});
} catch (e) {
setSnackbar({
open: true,
message: e instanceof Error ? e.message : "列印失敗",
message: e instanceof Error ? e.message : t("Print failed"),
severity: "error",
});
} finally {
setPrintingLotLineId(null);
}
},
[selectedPrinterId, printQty],
[selectedPrinterId, printQty, t],
);

return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>批號標籤列印(提貨台)</DialogTitle>
<DialogTitle>{t("Lot label print (pick station)")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
{statusTitleText ? (
@@ -548,13 +550,13 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
>
<TextField
inputRef={scanInputRef}
label="掃碼內容"
label={t("Scan content")}
value={scanInput}
onChange={(e) => setScanInput(e.target.value)}
fullWidth
size="small"
error={!!scanError}
helperText={scanError || "掃描後按 Enter 或點「查詢」"}
helperText={scanError || t("Scan then press Enter or click Look up")}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -568,7 +570,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
onClick={() => void handleAnalyze()}
disabled={analysisLoading || !scanInput.trim()}
>
{analysisLoading ? <CircularProgress size={18} /> : "查詢"}
{analysisLoading ? <CircularProgress size={18} /> : t("Look up")}
</Button>
<Button
variant="outlined"
@@ -578,7 +580,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
}}
disabled={analysisLoading}
>
清除
{t("Clear")}
</Button>
</Stack>
</>
@@ -594,16 +596,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
sx={{ minWidth: 260 }}
disabled={printersLoading}
>
<InputLabel>印表機</InputLabel>
<InputLabel>{t("Printer")}</InputLabel>
<Select
label="印表機"
label={t("Printer")}
value={selectedPrinterId}
onChange={(e) =>
setSelectedPrinterId((e.target.value as number) ?? "")
}
>
<MenuItem value="">
<em>{printersLoading ? "載入中..." : "請選擇"}</em>
<em>{printersLoading ? t("Loading") : t("Please select")}</em>
</MenuItem>
{printers.map((p) => (
<MenuItem key={p.id} value={p.id}>
@@ -614,7 +616,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
</FormControl>

<TextField
label="列印張數"
label={t("Print copies")}
size="small"
type="number"
inputProps={{ min: 1, step: 1 }}
@@ -626,7 +628,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =

{onWorkbenchScanPick ? (
<TextField
label="提交數量"
label={t("Submit Qty")}
size="small"
type="number"
inputProps={{ min: 0, step: 1 }}
@@ -651,7 +653,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
{analysisLoading ? (
<CircularProgress size={18} />
) : (
"刷新批號清單"
t("Refresh lot list")
)}
</Button>

@@ -661,7 +663,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
color="text.secondary"
sx={{ ml: { md: "auto" } }}
>
已選:{formatPrinterLabel(selectedPrinter)}
{t("Selected printer", { printer: formatPrinterLabel(selectedPrinter) })}
</Typography>
)}
</Stack>
@@ -669,12 +671,12 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
{analysis && (
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}>
品號:{analysis.itemCode} {analysis.itemName}
{t("Item code name", { code: analysis.itemCode, name: analysis.itemName })}
</Typography>

{filteredLots.length === 0 ? (
<Alert severity="warning">
找不到該樓層有可用批號(availableQty &gt; 0)。
{t("No available lots on this floor")}
</Alert>
) : (
<Stack spacing={1}>
@@ -717,14 +719,16 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
sx={{ fontWeight: lot._scanned ? 800 : 600 }}
>
Lot:{lot.lotNo}
{lot._scanned ? "(當前批次)" : ""}
{lot._scanned ? t(" (current lot)") : ""}
</Typography>
<Typography variant="body2" color="text.secondary">
位置:{loc || "—"}
{t("Location with value", { location: loc || "—" })}
</Typography>
<Typography variant="body2" color="text.secondary">
可用量:{Number(lot.availableQty).toLocaleString()}{" "}
單位:{lot.uom || ""}
{t("Available qty with uom", {
qty: Number(lot.availableQty).toLocaleString(),
uom: lot.uom || "",
})}
</Typography>
</Box>
<Stack
@@ -747,7 +751,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
{isPrinting ? (
<CircularProgress size={18} />
) : (
"列印標籤"
t("Print label")
)}
</Button>
{onWorkbenchScanPick ? (
@@ -756,9 +760,9 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
color="secondary"
title={
!lotQrPayload
? "此列無法取得 QR payload(需 stockInLineId)"
? t("This row has no QR payload")
: disableScanPick
? "此出庫行已掃碼或已完成,無法顯示 QR"
? t("This pick line already scanned or completed, QR cannot be shown")
: undefined
}
disabled={
@@ -772,7 +776,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
)
}
>
顯示 QR
{t("Show QR")}
</Button>
) : null}
</Stack>
@@ -809,14 +813,14 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
<Typography variant="body2" color="text.secondary">

{onWorkbenchScanPick
? "沒有任何批號可列印標籤"
? t("No lots available to print labels")
: ""}
</Typography>
)}
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>關閉</Button>
<Button onClick={onClose}>{t("Close")}</Button>
</DialogActions>

<Snackbar


+ 901
- 179
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


+ 4
- 0
src/components/InventorySearch/InventoryLotLineTable.tsx Parādīt failu

@@ -44,6 +44,7 @@ type AdjustmentEntry = InventoryLotLineResult & {
isNew?: boolean;
isOpeningInventory?: boolean;
remarks?: string;
uomId?: number;
};


@@ -154,6 +155,7 @@ const prevAdjustmentModalOpenRef = useRef(false);
adjustedQty: line.availableQty ?? 0,
originalQty: line.availableQty ?? 0,
remarks: '',
uomId: inventory.stockUomId ?? inventory.uomId,
}));
setAdjustmentEntries(initial);
originalAdjustmentLinesRef.current = initial;
@@ -273,6 +275,7 @@ const prevAdjustmentModalOpenRef = useRef(false);
expiryDate,
warehouseId: line.warehouse?.id ?? 0,
uom: line.uom ?? null,
uomId: line.uomId ?? null,
remarks: line.remarks?.trim() || null,
};
}, []);
@@ -342,6 +345,7 @@ const prevAdjustmentModalOpenRef = useRef(false);
status: 'available',
availableQty: addEntryForm.qty,
uom: inventory.uomUdfudesc || inventory.uomShortDesc || inventory.uomCode,
uomId: inventory.stockUomId ?? inventory.uomId,
qtyPerSmallestUnit: inventory.qtyPerSmallestUnit ?? 1,
baseUom: inventory.baseUom || '',
stockInLineId: 0,


+ 3
- 0
src/components/InventorySearch/InventorySearch.tsx Parādīt failu

@@ -33,6 +33,9 @@ type SearchQuery = Partial<
| "qty"
| "uomCode"
| "uomUdfudesc"
| "uomId"
| "stockUomId"
| "stockUomCode"
| "germPerSmallestUnit"
| "qtyPerSmallestUnit"
| "itemSmallestUnit"


+ 9
- 9
src/components/JoSave/JoRelease.tsx Parādīt failu

@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { JoDetailPickLine } from "@/app/api/jo";
import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket";
import { useEffect, useState, useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { JoDetail } from "@/app/api/jo";
@@ -50,15 +51,14 @@ const JoRelease: React.FC<Props> = ({
}, [pickLines]);

const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>
inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
);
if (inventory) {
return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
}
return 0;
return getStockAvailableFromInventories(inventoryData, {
itemId: pickLine.itemId,
itemCode: pickLine.code,
itemName: pickLine.name,
uomId: pickLine.uomId,
uom: pickLine.uom,
shortUom: pickLine.shortUom,
});
};

const isStockSufficient = (pickLine: JoDetailPickLine) => {


+ 9
- 9
src/components/JoSave/PickTable.tsx Parādīt failu

@@ -12,6 +12,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined';
import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket";
import { useEffect, useState } from "react";
import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';

@@ -54,15 +55,14 @@ const PickTable: React.FC<Props> = ({
}, [pickLines]);

const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>
inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
);
if (inventory) {
return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
}
return 0;
return getStockAvailableFromInventories(inventoryData, {
itemId: pickLine.itemId,
itemCode: pickLine.code,
itemName: pickLine.name,
uomId: pickLine.uomId,
uom: pickLine.uom,
shortUom: pickLine.shortUom,
});
};

const isStockSufficient = (pickLine: JoDetailPickLine) => {


+ 9
- 9
src/components/JoSearch/JoSearch.tsx Parādīt failu

@@ -25,6 +25,7 @@ import { msg } from "../Swal/CustomAlerts";
import dayjs from "dayjs";
//import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
@@ -132,15 +133,14 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
*/
const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>
inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
);
if (inventory) {
return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
}
return 0;
return getStockAvailableFromInventories(inventoryData, {
itemId: pickLine.itemId,
itemCode: pickLine.code,
itemName: pickLine.name,
uomId: pickLine.uomId,
uom: pickLine.uom,
shortUom: pickLine.shortUom,
});
};

const isStockSufficient = (pickLine: JoDetailPickLine) => {


+ 9
- 9
src/components/JoWorkbench/JoWorkbenchSearch.tsx Parādīt failu

@@ -26,6 +26,7 @@ import { msg } from "../Swal/CustomAlerts";
import dayjs from "dayjs";
//import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
@@ -133,15 +134,14 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo
*/
const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>
inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
);
if (inventory) {
return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
}
return 0;
return getStockAvailableFromInventories(inventoryData, {
itemId: pickLine.itemId,
itemCode: pickLine.code,
itemName: pickLine.name,
uomId: pickLine.uomId,
uom: pickLine.uom,
shortUom: pickLine.shortUom,
});
};

const isStockSufficient = (pickLine: JoDetailPickLine) => {


+ 4
- 4
src/components/PickOrderSearch/WorkbenchPickExecution.tsx Parādīt failu

@@ -654,11 +654,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
const reminder = workbenchLotLabelReminderText?.trim() ?? "";
if (reminder && isExpiredWorkbenchReminderMessage(reminder)) {
return { text: "此批號狀態:已過期", severity: "error" as const };
return { text: t("Lot status: expired"), severity: "error" as const };
}
const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot);
const s = getWorkbenchSourceLotStatusSummary(workbenchLotLabelContextLot, t);
return { text: s.text, severity: s.severity };
}, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText]);
}, [workbenchLotLabelModalOpen, workbenchLotLabelContextLot, workbenchLotLabelReminderText, t]);

const handleJustComplete = useCallback(
async (row: LotRow) => {
@@ -1714,7 +1714,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
onClick={() => openWorkbenchLotLabelModalForLot(r)}
sx={{ flexShrink: 0, fontSize: "0.7rem", py: 0.25, minWidth: "auto", px: 1, whiteSpace: "nowrap" }}
>
{t(" 批號 QR 碼")}
{t("lot QR code")}
</Button>
) : null}
</Stack>


+ 10
- 5
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Parādīt failu

@@ -36,6 +36,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli
import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { matchInventoryBucket, inventoryAvailableQty } from "@/app/api/inventory/inventoryBucket";
import { releaseJoForWorkbench } from "@/app/api/jo/workbenchActions";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
import ProcessSummaryHeader from "./ProcessSummaryHeader";
@@ -187,13 +188,17 @@ const getStockAvailable = (line: JobOrderLineInfo) => {
if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") {
return line.stockQty || 0;
}
const inventory = inventoryData.find(inv =>
inv.itemCode === line.itemCode || inv.itemName === line.itemName
);
const inventory = matchInventoryBucket(inventoryData, {
itemId: line.itemId,
itemCode: line.itemCode,
itemName: line.itemName,
stockUomId: line.stockUomId,
stockUom: line.stockUom,
});
if (inventory) {
return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
return inventoryAvailableQty(inventory);
}
return line.stockQty || 0;
return line.stockQty ?? 0;
};
const handleOpenPlanStartDialog = useCallback(() => {
// 将 processData.date 转换为 dayjs 对象


+ 1321
- 0
src/components/StockLedgerFix/StockLedgerFixPageClient.tsx
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


+ 59
- 32
src/components/charts/SafeApexCharts.tsx Parādīt failu

@@ -150,6 +150,34 @@ function buildApexConfig(

const EMPTY_MESSAGE = "暫無圖表資料(後端無法連線或此區間無資料)。";

/** Apex mutates `options` in place (circular refs). Never throw during render. */
function safeStringify(value: unknown): string {
const seen = new WeakSet<object>();
try {
return JSON.stringify(value, (_key, nested) => {
if (typeof nested === "function") return undefined;
if (typeof nested === "object" && nested !== null) {
if (seen.has(nested)) return undefined;
seen.add(nested);
}
return nested;
});
} catch {
return "";
}
}

function destroyChart(chartRef: { current: { destroy: () => void } | null }) {
const current = chartRef.current;
chartRef.current = null;
if (!current) return;
try {
current.destroy();
} catch {
/* ignore */
}
}

export default function SafeApexCharts(props: SafeApexChartsProps) {
const { type, series, options, height, width, chartRevision, allowZeroSeries = false, ...rest } = props;
const containerRef = useRef<HTMLDivElement>(null);
@@ -162,18 +190,11 @@ export default function SafeApexCharts(props: SafeApexChartsProps) {
sanitized = alignSeriesToCategoryCount(sanitized, cats.length);
}

if (shouldShowPlaceholder(type, options, sanitized, allowZeroSeries)) {
return (
<Typography color="text.secondary" sx={{ py: 3 }}>
{EMPTY_MESSAGE}
</Typography>
);
}

let chartOptions = options;
let renderSeries: ApexChartProps["series"] = sanitized;
let showPlaceholder = shouldShowPlaceholder(type, options, sanitized, allowZeroSeries);

if (isRadialChart(type) && isNumberArray(sanitized)) {
if (!showPlaceholder && isRadialChart(type) && isNumberArray(sanitized)) {
const prev = Array.isArray(options?.labels) ? (options!.labels as unknown[]) : [];
const pairs = sanitized.map((v, i) => {
const n = Number.isFinite(v) ? v : 0;
@@ -183,20 +204,31 @@ export default function SafeApexCharts(props: SafeApexChartsProps) {
});
const nonzero = pairs.filter((p) => p.v > 0);
if (nonzero.length === 0) {
return (
<Typography color="text.secondary" sx={{ py: 3 }}>
{EMPTY_MESSAGE}
</Typography>
);
showPlaceholder = true;
} else {
renderSeries = nonzero.map((p) => p.v);
chartOptions = { ...options, labels: nonzero.map((p) => p.label) };
}
renderSeries = nonzero.map((p) => p.v);
chartOptions = { ...options, labels: nonzero.map((p) => p.label) };
}

const chartType = String(type ?? "line");
const configSnapshot = `${String(chartRevision ?? "")}|${chartType}|${JSON.stringify(renderSeries ?? null)}|${JSON.stringify(chartOptions ?? {})}|${String(height ?? "")}|${String(width ?? "")}`;
const configSnapshot = [
String(chartRevision ?? ""),
chartType,
String(showPlaceholder),
safeStringify(renderSeries ?? null),
safeStringify(cats ?? null),
safeStringify(chartOptions?.labels ?? null),
String(height ?? ""),
String(width ?? ""),
].join("|");

useEffect(() => {
if (showPlaceholder) {
destroyChart(chartRef);
return;
}

const el = containerRef.current;
if (!el) return;

@@ -219,12 +251,7 @@ export default function SafeApexCharts(props: SafeApexChartsProps) {

if (disposed || containerRef.current !== el) {
if (chartRef.current === instance) {
try {
instance.destroy();
} catch {
/* ignore */
}
chartRef.current = null;
destroyChart(chartRef);
}
return;
}
@@ -236,21 +263,21 @@ export default function SafeApexCharts(props: SafeApexChartsProps) {

return () => {
disposed = true;
const c = chartRef.current;
chartRef.current = null;
if (c) {
try {
c.destroy();
} catch {
/* ignore */
}
}
destroyChart(chartRef);
};
}, [configSnapshot]);

const minH = typeof height === "number" ? height : typeof height === "string" ? height : 240;
const dom = rest as { className?: string; id?: string; style?: CSSProperties; sx?: object };

if (showPlaceholder) {
return (
<Typography color="text.secondary" sx={{ py: 3 }}>
{EMPTY_MESSAGE}
</Typography>
);
}

return (
<Box
ref={containerRef}


+ 110
- 110
src/i18n/en/common.json Parādīt failu

@@ -1,131 +1,131 @@
{
"Actions": "操作",
"Add Document": "新增文件",
"Actions": "Actions",
"Add Document": "Add Document",
"All": "All",
"Allergic Substances": "過敏原",
"Allergic Substances": "Allergens",
"An error has occurred. Please try again later.": "An error has occurred. Please try again later.",
"Are you sure you want to delete this item?": "您確定要刪除此項目嗎?",
"Back": "返回",
"Basic Info": "基本資訊",
"Are you sure you want to delete this item?": "Are you sure you want to delete this item?",
"Back": "Back",
"Basic Info": "Basic Info",
"Bom Required Qty": "BOM Required Qty",
"Bom UOM": "BOM UOM",
"Brand": "品牌",
"CMB": "消耗品",
"CO": "消耗品",
"Cancel": "取消",
"Column Name": "欄位名稱",
"Coming soon": "即將推出",
"Complexity": "複雜度",
"Confirm": "確認",
"Confirm Delete": "確認刪除",
"Cost (HKD)": "費用 (HKD)",
"Current total": "目前總和",
"Day Before Yesterday": "前天",
"Delete": "刪除",
"Delete Failed": "刪除失敗",
"Brand": "Brand",
"CMB": "Consumable",
"CO": "Consumable",
"Cancel": "Cancel",
"Column Name": "Column Name",
"Coming soon": "Coming soon",
"Complexity": "Complexity",
"Confirm": "Confirm",
"Confirm Delete": "Confirm Delete",
"Cost (HKD)": "Cost (HKD)",
"Current total": "Current total",
"Day Before Yesterday": "Day Before Yesterday",
"Delete": "Delete",
"Delete Failed": "Delete Failed",
"Do you want to delete?": "Do you want to delete?",
"Density": "濃淡",
"Depth": "顔色深淺度 深1淺5",
"Description": "描述",
"Details": "詳情",
"Duration (Minutes)": "時間(分)",
"Edit": "編輯",
"Enter any additional observations or notes...": "輸入其他觀察或備註...",
"Enter or select remark": "輸入或選擇備註",
"Density": "Density",
"Depth": "Color depth (dark 1 / light 5)",
"Description": "Description",
"Details": "Details",
"Duration (Minutes)": "Duration (Minutes)",
"Edit": "Edit",
"Enter any additional observations or notes...": "Enter any additional observations or notes...",
"Enter or select remark": "Enter or select remark",
"Error saving data": "Error saving data",
"Failed to fetch data": "無法取得資料",
"Filter": "過濾",
"Finished Good Detail": "成品出倉詳情",
"Finished Good Management": "成品出倉管理",
"Finished Good Order": "成品出倉",
"Float": "浮沉",
"General Data": "基本資料",
"Grade {{grade}}": "等級 {{grade}}",
"Invoice": "發票",
"Invoice Date": "發票日期",
"Failed to fetch data": "Failed to fetch data",
"Filter": "Filter",
"Finished Good Detail": "Finished Good Detail",
"Finished Good Management": "Finished Good Management",
"Finished Good Order": "Finished Good Order",
"Float": "Float",
"General Data": "General Data",
"Grade {{grade}}": "Grade {{grade}}",
"Invoice": "Invoice",
"Invoice Date": "Invoice Date",
"IP": "IP",
"Item Code": "Item Code",
"Item Name": "Item Name",
"Loading": "載入中...",
"Loading order summary": "正在載入訂單摘要",
"Location": "位置",
"MA": "材料",
"MAT": "材料",
"MI": "雜項",
"Material Name": "材料清單",
"Loading": "Loading...",
"Loading order summary": "Loading order summary",
"Location": "Location",
"MA": "Material",
"MAT": "Material",
"MI": "Miscellaneous",
"Material Name": "Material List",
"Name": "Name",
"Min": "最小值",
"NM": "雜項及非消耗品",
"No": "",
"No Lot": "沒有批號",
"No data available": "沒有資料",
"No options": "沒有選項",
"Order": "順序",
"Min": "Min",
"NM": "Miscellaneous and non-consumables",
"No": "No",
"No Lot": "No Lot",
"No data available": "No data available",
"No options": "No options",
"Order": "Order",
"Pending": "Pending",
"Port": "Port",
"Please Select BOM": "請選擇 BOM",
"Please try again later.": "請稍後重試。",
"Project Code": "專案代碼",
"Project Code and Name": "專案代碼與名稱",
"QC Template not found": "找不到 QC 範本",
"Qty": "數量",
"RM": "原料",
"Range": "範圍",
"Refresh": "重新載入",
"Remarks": "備註",
"Remove Document": "移除文件",
"Report": "報告",
"Please Select BOM": "Please select BOM",
"Please try again later.": "Please try again later.",
"Project Code": "Project Code",
"Project Code and Name": "Project Code and Name",
"QC Template not found": "QC template not found",
"Qty": "Qty",
"RM": "Raw material",
"Range": "Range",
"Refresh": "Refresh",
"Remarks": "Remarks",
"Remove Document": "Remove Document",
"Report": "Report",
"Reset": "Reset",
"Row per page": "每頁行數",
"Rows per page": "每頁行數",
"Sales Qty": "銷售數量",
"Sales UOM": "銷售單位",
"Save": "儲存",
"Saving": "儲存中",
"Row per page": "Rows per page",
"Rows per page": "Rows per page",
"Sales Qty": "Sales Qty",
"Sales UOM": "Sales UOM",
"Save": "Save",
"Saving": "Saving",
"Search": "Search",
"Search Criteria": "Search Criteria",
"Select Date": "選擇日期",
"Session expired or unauthorized.": "工作階段已過期或未經授權。",
"Select Date": "Select Date",
"Session expired or unauthorized.": "Session expired or unauthorized.",
"Sign out": "Sign out",
"Language": "Language",
"Status": "狀態",
"Stock Qty": "庫存數量",
"Supporting Document": "證明文件",
"Task": "任務",
"Time Sequence": "時段",
"Status": "Status",
"Stock Qty": "Stock Qty",
"Supporting Document": "Supporting Document",
"Task": "Task",
"Time Sequence": "Time Sequence",
"Type": "Type",
"Today": "今天",
"Total weighting must equal 1": "權重總和必須等於 1",
"Unauthorized: Please log in again": "未經授權:請重新登入",
"Uom": "單位",
"Update Failed": "更新失敗",
"Update Success": "更新成功",
"Weighting must be a number": "權重必須為數字",
"Yes": "",
"Yesterday": "昨天",
"all": "全部",
"Today": "Today",
"Total weighting must equal 1": "Total weighting must equal 1",
"Unauthorized: Please log in again": "Unauthorized: Please log in again",
"Uom": "UOM",
"Update Failed": "Update Failed",
"Update Success": "Update Success",
"Weighting must be a number": "Weighting must be a number",
"Yes": "Yes",
"Yesterday": "Yesterday",
"all": "All",
"bomWeighting": "BOM Weighting Score",
"cmb": "消耗品",
"collapsible table": "可折疊表格",
"consumable": "消耗品",
"consumables": "消耗品",
"create": "新增",
"edit": "編輯",
"expand row": "展開行",
"group mode": "群組模式",
"item": "貨品",
"items": "物品",
"mat": "原料",
"menu": "選單",
"nm": "雜項及非消耗品",
"non-consumables": "非消耗品",
"other": "其他",
"profile": "個人資料",
"revert": "還原",
"settings": "設定",
"stockRecord": "盤點記錄",
"stocktakemanagement": "盤點管理",
"testing sections tabs": "測試區域分頁",
"warehouse": "倉庫",
"材料": "材料"
"cmb": "Consumable",
"collapsible table": "Collapsible table",
"consumable": "Consumable",
"consumables": "Consumables",
"create": "Create",
"edit": "Edit",
"expand row": "Expand row",
"group mode": "Group mode",
"item": "Item",
"items": "Items",
"mat": "Raw material",
"menu": "Menu",
"nm": "Miscellaneous and non-consumables",
"non-consumables": "Non-consumables",
"other": "Other",
"profile": "Profile",
"revert": "Revert",
"settings": "Settings",
"stockRecord": "Stock record",
"stocktakemanagement": "Stock take management",
"testing sections tabs": "Testing section tabs",
"warehouse": "Warehouse",
"材料": "Material"
}

+ 43
- 1
src/i18n/en/doWorkbench.json Parādīt failu

@@ -45,5 +45,47 @@
"DO Workbench Search": "DO Workbench Search",
"completed": "completed",
"items": "items",
"pending": "pending"
"pending": "pending",
"Pick Order Detail": "Pick Detail",
"Etra Pick Order Detail": "Etra",
"Finished Good Record": "FG Record",
"Finished Good Record (All)": "FG Record (All)",
"Ticket Release Table": "Ticket Release",
"FG Carton Qty": "Carton Qty",
"成品出倉出箱數量": "Carton Qty",
"Truck Routing Summary": "Routing Summary",
"送貨路線摘要": "Truck Routing Summary",
"車線-X": "Truck X",
"Confirm print drafts": "Print {{count}} draft(s)?",
"Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)",
"2/F or 4/F": "2/F or 4/F",
"Lane": "Lane",
"Shop Code": "Shop group",
"Shop code": "Shop code",
"Back to previous level": "Back",
"All lanes": "All lanes",
"All shops": "All shop groups",
"Cartons by lane": "Cartons by lane",
"Cartons by shop": "Cartons by shop",
"Cartons by shop group": "Cartons by shop group",
"Cartons by floor": "Cartons by floor",
"Share": "Share",
"Click a row to filter": "Click a row to filter",
"Click a bar to filter": "Click a bar to filter",
"Reset filters": "Reset filters",
"Cartons": "Cartons",
"Date": "Date",
"Download Excel": "Download Excel",
"Download this view Excel": "Download Excel (Selected)",
"Download period Excel": "Download Excel (Complete)",
"Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.",
"Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.",
"Filtered": "Filtered",
"Breakdown": "Breakdown",
"All floors": "All floors",
"FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}",
"Generating...": "Generating...",
"Download report (PDF)": "Download report (PDF)",
"Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?",
"Failed to download Workbench truck routing summary. Please try again later.": "Failed to download Workbench truck routing summary. Please try again later."
}

+ 2
- 0
src/i18n/en/navigation.json Parādīt failu

@@ -49,6 +49,8 @@
"nav.settings.demandForecast": "Demand Forecast Setting",
"nav.settings.bomWeighting": "BOM Weighting Score List",
"nav.settings.masterDataIssues": "BOM / Item UOM Issues",
"nav.settings.stockLedgerFix": "Stock Ledger Fix",
"nav.breadcrumb.stockLedgerFix": "Stock Ledger Fix",
"nav.settings.qrCodeHandle": "QR Code Handle",
"nav.settings.importTesting": "Import Testing",
"nav.settings.importExcel": "Import Excel",


+ 103
- 9
src/i18n/en/pickOrder.json Parādīt failu

@@ -58,7 +58,7 @@
"Lines": "Lines",
"Before Today": "Before Today",
"Truck X": "Truck X",
"Finsihed good items": "Finsihed good items",
"Finsihed good items": "Finished good items",
"kinds": "kinds",
"Completed Date": "Completed Date",
"Completed Time": "Completed Time",
@@ -147,8 +147,8 @@
"Etra": "Etra",
"Exit Etra view": "Exit Etra view",
"Etra Pick Order Detail": "Etra Pick Order Detail",
"Etra incomplete badge tooltip": "Etra incomplete badge tooltip",
"Etra incomplete badge tooltip none": "Etra incomplete badge tooltip none",
"Etra incomplete badge tooltip": "Incomplete extra tickets today: {{count}} (pending/released, excluding completed)",
"Etra incomplete badge tooltip none": "No incomplete extra tickets",
"Back to normal assign tab": "Back to normal assign tab",
"Enter isExtra workbench view?": "Enter isExtra workbench view?",
"Etra view groups all add-on tickets by shop and lane for the selected date.": "Etra view groups all add-on tickets by shop and lane for the selected date.",
@@ -179,7 +179,6 @@
"Confirm Search": "Search",
"Merge Etra ticket search prompt": "Enter shop (optional) and date, then click Search to load merge candidates.",
"Merge Etra ticket search failed": "Failed to load merge candidates. Ensure the backend is updated and restarted.",
"Truck X": "Truck X",
"Pick Order": "Pick Order",
"Type": "Type",
"Product Type": "Product Type",
@@ -529,14 +528,14 @@
"Floor ticket": "Floor ticket",
"2F ticket": "2F ticket",
"4F ticket": "4F ticket",
"4F lane panel legend": "4F lane panel legend",
"Loading sequence n": "Loading sequence n",
"lot QR code": "lot QR code",
"4F lane panel legend": "Lane — loading sequence (unassigned/total)",
"Loading sequence n": "Board {{n}}",
"lot QR code": "Lot QR Code",
"label Printer": "label Printer",
"Loading Sequence": "Loading Sequence",
"Ticket No": "Ticket No",
"The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.",
"is unavable. Please check around have available QR code or not.": "is unavable. Please check around have available QR code or not.",
"is unavable. Please check around have available QR code or not.": "This lot is unavailable. Please check around for an available QR code.",
"Lot switch failed; pick line was not marked as checked.": "Lot switch failed; pick line was not marked as checked.",
"Lot confirmation failed. Please try again.": "Lot confirmation failed. Please try again.",
"Powder Mixture": "Powder Mixture",
@@ -559,5 +558,100 @@
"passed": "Passed",
"failed": "Failed",
"confirm_accept_with_fail": "There are failed QC items. Confirm to accept stock out?",
"No pending pick line left for this item. It may already be completed or fully processed.": "No pending pick line left for this item. It may already be completed or fully processed."
"No pending pick line left for this item. It may already be completed or fully processed.": "No pending pick line left for this item. It may already be completed or fully processed.",
"FG Carton Qty": "FG Carton Qty",
"成品出倉出箱數量": "FG Carton Qty",
"Truck Routing Summary": "Truck Routing Summary",
"送貨路線摘要": "Truck Routing Summary",
"車線-X": "Truck X",
"Lot QR Code": "Lot QR Code",
" 批號 QR 碼": "Lot QR Code",
"Cannot determine this lot status": "Cannot determine this lot status",
"Lot status: expired": "Lot status: expired",
"This pick line was rejected. Please scan another lot.": "This pick line was rejected. Please scan another lot.",
"This pick line is already completed. No further pick needed.": "This pick line is already completed. No further pick needed.",
"No lot bound / no available stock line. Please scan nearby inbound or transfer QR.": "No lot bound / no available stock line. Please scan nearby inbound or transfer QR.",
"Lot status: depleted (no remaining stock)": "Lot status: depleted (no remaining stock)",
"Lot status: depleted (available qty is 0)": "Lot status: depleted (available qty is 0)",
"Lot status: unavailable (not put away or line unavailable)": "Lot status: unavailable (not put away or line unavailable)",
"Lot status: ready to pick": "Lot status: ready to pick",
"This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.": "This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.",
"No lot information for this item in the current order": "No lot information for this item in the current order",
"Lot label print (pick station)": "Lot label print (pick station)",
"Scan content": "Scan content",
"Scan then press Enter or click Look up": "Scan then press Enter or click Look up",
"Look up": "Look up",
"Clear": "Clear",
"Printer": "Printer",
"Please select": "Please select",
"Print copies": "Print copies",
"Refresh lot list": "Refresh lot list",
"Selected printer": "Selected: {{printer}}",
"Item code name": "Item: {{code}} {{name}}",
"No available lots on this floor": "No available lots on this floor (available qty > 0).",
" (current lot)": " (current lot)",
"Location with value": "Location: {{location}}",
"Available qty with uom": "Available qty: {{qty}} UOM: {{uom}}",
"Print label": "Print label",
"Show QR": "Show QR",
"This row has no QR payload": "This row has no QR payload (stockInLineId required)",
"This pick line already scanned or completed, QR cannot be shown": "This pick line is already scanned or completed, so QR cannot be shown",
"No lots available to print labels": "No lots available to print labels",
"Failed to load printer list": "Failed to load printer list",
"Loaded available lots for this item": "Loaded available lots for this item",
"Analysis failed": "Look-up failed",
"Invalid itemId, cannot load lot list.": "Invalid item ID, cannot load lot list.",
"Invalid scan format. Please scan again.": "Invalid scan format. Please scan again.",
"Scan or look up once before refreshing the lot list.": "Scan or look up once before refreshing the lot list.",
"Print quantity must be an integer of 1 or more": "Print quantity must be an integer of 1 or more",
"Print sent: Lot {{lotNo}}": "Print sent: Lot {{lotNo}}",
"Print failed": "Print failed",
"Failed to load FG carton quantity. Please try again later.": "Failed to load FG carton quantity. Please try again later.",
"Cartons": "Cartons",
"{{count}} cartons": "{{count}} cartons",
"No chart data": "No chart data",
"2/F carton qty": "2/F carton qty",
"4/F carton qty": "4/F carton qty",
"Truck X carton qty": "Truck X carton qty",
"Total carton qty": "Total carton qty",
"Summary": "Summary",
"All floors": "All floors",
"Last 7 days": "Last 7 days",
"This month": "This month",
"This year": "This year",
"FG carton qty last 7 days title": "FG carton qty (last 7 days) - {{floor}} - as of {{date}}",
"FG carton qty this month title": "FG carton qty (this month) - {{floor}} - {{period}}",
"FG carton qty this year title": "FG carton qty (this year) - {{floor}} - {{period}}",
"Exporting...": "Exporting...",
"Download Excel": "Download Excel",
"Download this view Excel": "Download Excel (Selected)",
"Download period Excel": "Download Excel (Complete)",
"Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.",
"Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.",
"Filtered": "Filtered",
"Breakdown": "Breakdown",
"FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}",
"Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)",
"2/F or 4/F": "2/F or 4/F",
"Lane": "Lane",
"Shop Code": "Shop group",
"Shop code": "Shop code",
"Back to previous level": "Back",
"All lanes": "All lanes",
"All shops": "All shop groups",
"Cartons by lane": "Cartons by lane",
"Cartons by shop": "Cartons by shop",
"Cartons by shop group": "Cartons by shop group",
"Cartons by floor": "Cartons by floor",
"Share": "Share",
"Click a row to filter": "Click a row to filter",
"Click a bar to filter": "Click a bar to filter",
"Reset filters": "Reset filters",
"Generating...": "Generating...",
"Download report (PDF)": "Download report (PDF)",
"Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?",
"Failed to download Workbench truck routing summary. Please try again later.": "Failed to download Workbench truck routing summary. Please try again later.",
"Confirm print drafts": "Print {{count}} draft(s)?",
"Floor": "Floor",
"All": "All"
}

+ 147
- 0
src/i18n/en/stockLedgerFix.json Parādīt failu

@@ -0,0 +1,147 @@
{
"pageTitle": "Stock Ledger Fix",
"inventory10Title": "1.0 Inventory (Plan A)",
"inventory10Description": "Keeps existing IDs. Adds UOMs that exist on lots but not in inventory. onHand = Σ(in−out) by item+UOM. Rows with no lots become 0. When an item has multiple old rows, only one stock-UOM bucket is kept; the rest are marked deleted=1. Run this while inbound/outbound is paused, then Fix day by day.",
"inventory10LoadError": "Could not load inventory 1.0 preview (ADMIN / TESTING required)",
"inventory10Preview": "Existing inventory: {{rows}} rows; lot item+UOM pairs: {{lotPairs}}; missing: {{missing}}; stockUomId still NULL: {{nullUom}} rows.",
"inventory10Running": "Recalculating…",
"inventory10Run": "Run inventory 1.0 (A)",
"inventory10Confirm": "Plan A: keep existing inventory.id; backfill stockUomId from lots (if the old uomId is stock, revert it to base); insert missing (itemId, stockUomId) rows; recalculate onHand/unavailable from lot in−out. Does not write onHoldQty or change inventory_lot_line. Continue?",
"inventory10Done": "Inventory 1.0 complete: patched stockUomId {{patched}} rows, inserted {{inserted}}, orphans deleted {{orphans}}, recalculated {{updated}}, still missing UOM {{missingAfter}}, still NULL stockUomId {{nullAfter}}",
"inventory10Fail": "Inventory 1.0 failed",
"adjTitle": "2.7 ADJ: set over-issue remain to 0, then align ledger",
"adjDescription": "Choose an ADJ date, then preview. Default is yesterday. For a freeze-dump night you can pick today (e.g. write 31/8 23:17 ADJ onto 31/8). First handle over-issue (line in < out): raise that lot’s inQty to equal outQty (remain=0; trigger recalculates inventory). Then handle miss: if line and ledger differ, write ADJ against the existing lot’s inbound/outbound lines (lot quantities are not changed). One Apply shares one inbound document and one outbound document, with multiple lines under them. Running again on the same ADJ date reuses those two documents. Detail shows at most 20 rows.",
"adjDateLabel": "ADJ date",
"adjDateRequired": "Please enter an ADJ date",
"adjDateFuture": "ADJ date cannot be in the future",
"adjDateAndPreviewRequired": "Please enter an ADJ date and preview first",
"adjTodayFreeze": "Today (freeze night)",
"adjYesterday": "Yesterday",
"adjLoadError": "Could not load ADJ preview (ADMIN / TESTING required)",
"adjPreviewSummary": "ADJ date {{date}}; {{lotCount}} lots can be processed (over-issue remain→0: {{overIssueCount}}, total {{sumOverIssue}}; miss in {{adjInCount}}, out {{adjOutCount}}); missing in total {{sumMissIn}}, missing out total {{sumMissOut}}; SKU net {{skuNet}}{{skuNetNote}}{{revNote}}. Table shows at most 20 rows (over-issue first).",
"adjSkuNetNote": " (non-zero means missing in has no matching missing out)",
"adjRevNote": "; includes reverse ADJ {{count}} lots (ledger over-recorded)",
"adjColLotLineId": "lotLineId",
"adjColItemCode": "itemCode",
"adjColLineInOut": "line in/out",
"adjColLedgerInOut": "ledger in/out",
"adjColMissIn": "Missing in",
"adjColMissOut": "Missing out",
"adjColOverIssue": "Over-issue",
"adjPreviewing": "Previewing…",
"adjPreviewAgain": "Refresh preview",
"adjPreview": "Preview",
"adjApplying": "Writing ADJ…",
"adjApply": "Apply ADJ",
"adjConfirm": "On {{date}}, first patch over-issue remain→0 ({{overIssueCount}} lots, total {{sumOverIssue}}; this changes lot inQty). Then fill miss ADJ: inbound {{adjInCount}}, outbound {{adjOutCount}}. All inbound lines share one inbound document; all outbound lines share one outbound document (miss does not change lot qty). Missing in total {{sumMissIn}}, missing out total {{sumMissOut}}. Continue?",
"adjDone": "ADJ {{date}}: over-issue remain→0 {{overIssuePatched}}, in {{insertedIn}}, out {{insertedOut}}, lotQty {{filledLotQty}}, balance {{filledBalance}}, day close {{dayRowsWritten}}",
"adjFail": "ADJ align failed",
"tabCalendar": "Calendar",
"tabInventory": "Inventory",
"tabLot": "Lot line",
"calendarHint": "Click the title at the top-left to pick year, then month.\nChanging month does not load data; click a day to inspect that day.\nToday can be fixed (freeze-dump night).",
"selectDate": "Select a date",
"dayLoadError": "Could not load the day’s check",
"dayLedgerCount": "{{cnt}} ledger rows that day",
"cannotFixFuture": "Cannot fix a future date",
"cannotFixFutureRange": "Cannot fix a future date (today is allowed for freeze-dump night)",
"pickAtLeastOneStep": "Select at least one step (default runs all of 2.1–2.6)",
"allSteps216": "all 2.1–2.6",
"confirmPartialSteps": "Run only {{steps}} ({{date}}). Unchecked steps will not be recalculated. Continue?",
"fixDayDone": "Fixed {{date}} ({{steps}}): lot {{lot}}, uom {{uom}}, inventory {{inventory}}, lotQty {{lotQty}}, balance {{balance}}, day close {{dayRows}}",
"fixFailed": "Fix failed",
"fixing": "Fixing…",
"fixThisDay": "Fix this day",
"stepsTitle": "Steps (all selected by default)",
"stepsHint": "After applying SQL, if you only need to change inventoryId, tick 2.3 only. Unchecked steps are not recalculated.",
"selectAll": "Select all",
"step21": "2.1 lot",
"step22": "2.2 uom",
"step23": "2.3 inventoryId",
"step24": "2.4 lotQty",
"step25": "2.5 balance",
"step26": "2.6 day close",
"rangeTitle": "Range fix (2.1–2.6 in one run)",
"rangeHint": "Uses the steps ticked above. One API call, but runs day by day (same SQL as a single-day fix, commit per day). Today is allowed (freeze-dump night). Frontend timeout is about 20 minutes; for a large range, export SQL instead.",
"rangeFromRequired": "Please fill range from / to",
"toMustBeGteFrom": "to must be ≥ from",
"fromTooEarly": "from cannot be earlier than {{date}}",
"rangeConfirm": "Fix {{from}} → {{to}} (steps: {{steps}}). Runs day by day (commit each day). Days already finished are kept if it fails mid-way. Continue?",
"rangeProgress": "Range fix {{from}} → {{to}} ({{steps}}) in progress…",
"rangeDone": "Range fix complete {{date}} ({{steps}}): lot {{lot}}, uom {{uom}}, inventory {{inventory}}, lotQty {{lotQty}}, balance {{balance}}, day close {{dayRows}}",
"rangeFail": "Range fix failed",
"rangeRunning": "Range fix in progress…",
"rangeRun": "Range fix",
"from": "from",
"to": "to",
"exportTitle": "Export patched SQL (for the new database)",
"exportHint": "Tick the sections to export (same as day-fix steps). Apply order: 1.0 → ledger (±2.3) → 2.7 inbound/outbound documents + ADJ INSERT → 2.6. A full file (1.0 + 2.3) can skip production 1.0/2.3 when production is frozen and IDs match. Default exports only ledger + day close (legacy behaviour).",
"exportPickAtLeastOne": "Select at least one export section",
"export23Without10": "2.3 inventoryId is ticked but 1.0 inventory is not: if production is missing the new IDs, UPDATE will point at empty rows. Tick 1.0 as well (full file). Export anyway?",
"exportFail": "Export failed",
"exporting": "Exporting…",
"exportSql": "Export .sql",
"exportDefault": "Default",
"exportFull": "Full file",
"exportFromToRequired": "Please fill from / to",
"export10": "1.0 inventory",
"export23": "2.3 inventoryId",
"exportLedger": "ledger lot/uom/lotQty/balance",
"export26": "2.6 day close",
"export27": "2.7 ADJ + inbound/outbound documents",
"invTabHint": "Search by itemCode or inventoryId. Fix recalculates lotQty, day close, and balance for this item’s ledger up to today — no need to click the calendar day by day. If the warehouse is still writing this item during the day, new ledger rows after the fix are still handled by the live writer.",
"invSearchLabel": "itemCode / inventoryId",
"searching": "Searching…",
"search": "Search",
"searchFailed": "Search failed",
"colInventoryId": "inventoryId",
"colItemCode": "itemCode",
"colUomId": "uomId",
"colLedgerRows": "ledger rows",
"invLoadError": "Could not load inventory",
"invScopeSummary": "{{itemCode}} / inventory {{id}} / uom {{uomId}}; {{firstDate}} → {{lastDate}}; {{cnt}} rows; last balance {{lastBalance}}",
"invFixConfirm": "This will recalculate lotQty and day close for all ledger rows of this inventory up to today, plus this item’s balance. Continue?",
"invFixDone": "Fixed inventory {{id}}: lot {{lot}}, uom {{uom}}, inventory {{inventory}}, lotQty {{lotQty}}, balance {{balance}}, day close {{dayRows}}",
"fixThisInventory": "Fix this inventory",
"lotTabHint": "Search by lotNo or inventoryLotLineId. Fix only recalculates this lot’s lotQty and day close; it does not change balance.",
"lotSearchLabel": "lotNo / lot line id",
"colLotLineId": "lot line id",
"colLotNo": "lotNo",
"lotLoadError": "Could not load lot",
"lotScopeSummary": "{{lotNo}} / lot {{id}} / {{itemCode}}; {{firstDate}} → {{lastDate}}; {{cnt}} rows; last lotQtyAfter {{lastLotQtyAfter}}",
"lotFixConfirm": "This only fixes this lot’s lotQty and day close; it will not change the item’s balance. Continue?",
"lotFixDone": "Fixed lot {{id}}: lot {{lot}}, uom {{uom}}, inventory {{inventory}}, lotQty {{lotQty}}, day close {{dayRows}}",
"fixThisLot": "Fix this lot",
"checkItem": "Item",
"checkStatus": "Status",
"checkCorrect": "Correct",
"checkMiss": "Missing",
"checkIncorrect": "Incorrect",
"checkReason": "Reason",
"checkRows": "Rows",
"checkDash": "—",
"canAutoFix": "Can auto-fix (handled by Fix)",
"cannotAutoFix": "Cannot auto-fix (Fix will not clear these: old TKE new lots / true over-issue / no lot source)",
"verdictCorrect": "correct",
"verdictMiss": "miss",
"verdictOverIssue": "over-issue",
"verdictCanFix": "can fix",
"verdictCannotFix": "cannot fix",
"verdictIncorrect": "incorrect",
"part": {
"lot": "inventoryLotLineId",
"uom": "uomId",
"inventory": "inventoryId",
"lotQty": "lotQtyBefore/After (lot already exists)",
"overIssue": "over-issue (lotQty < 0)",
"balance": "balance",
"dayTable": "stock_lot_day",
"fixable21a": "Can fix 2.1a: SIL/SOL already has a lot line",
"fixable21b": "Can fix 2.1b: inventoryLotId has only one line",
"fixableLotQty": "Can fix: lot exists but lotQty is missing",
"cannotNoSource": "Cannot fix: no SIL/SOL lot source",
"cannotMultiLine": "Cannot fix: same lot has multiple warehouse lines",
"earlyTke": "Cannot fix: old TKE opened a new lot (sibling surplus)",
"realOver": "Cannot fix: over-issue is not old TKE (true over-issue / ledger mismatch)"
}
}

+ 3
- 3
src/i18n/en/ticketReleaseTable.json Parādīt failu

@@ -16,11 +16,11 @@
"Departure Time": "Departure Time",
"Floor": "Floor",
"Force complete DO": "Force complete DO",
"Force complete hint": "Force complete hint",
"Force complete hint": "Marks the ticket completed and archived without changing picked quantities. Use when all lines are submitted but the system did not complete.",
"Handler Name": "Handler Name",
"Last updated": "Last updated",
"Loading Sequence": "Loading Sequence",
"Manager only hint": "Manager only hint",
"Manager only hint": "Admin only",
"No data available": "No data available",
"Now": "Now",
"Number of FG Items (Order Item(s) Count)": "Number of FG Items (Order Item(s) Count)",
@@ -29,7 +29,7 @@
"Reload data": "Reload data",
"Required Delivery Date": "Required Delivery Date",
"Revert assignment": "Revert assignment",
"Revert assignment hint": "Revert assignment hint",
"Revert assignment hint": "Clears the assigned handler so the ticket returns to unassigned and can be taken again.",
"Rows per page": "Rows per page",
"Select All": "Select All",
"Select Date": "Select Date",


+ 38
- 1
src/i18n/zh/doWorkbench.json Parādīt failu

@@ -45,5 +45,42 @@
"Auto-refresh every 5 minutes": "每5分鐘自動刷新",
"Last updated": "最後更新",
"Truck Information": "車線資訊",
"Actions": "操作"
"Actions": "操作",
"FG Carton Qty": "成品出倉出箱數量",
"成品出倉出箱數量": "成品出倉出箱數量",
"Truck Routing Summary": "送貨路線摘要",
"送貨路線摘要": "送貨路線摘要",
"車線-X": "車線-X",
"Confirm print drafts": "確認列印 {{count}} 張草稿?",
"Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)",
"2/F or 4/F": "2/F 或 4/F",
"Lane": "車線",
"Shop Code": "店鋪組別",
"Shop code": "店鋪編號",
"Back to previous level": "返回上一層",
"All lanes": "全部車線",
"All shops": "全部店鋪組別",
"Cartons by lane": "按車線統計箱數",
"Cartons by shop": "按店鋪編號統計箱數",
"Cartons by shop group": "按店鋪組別統計箱數",
"Cartons by floor": "按樓層統計箱數",
"Share": "佔比",
"Click a row to filter": "點選列以篩選",
"Click a bar to filter": "點選長條以篩選",
"Reset filters": "重設篩選",
"Cartons": "箱數",
"Date": "日期",
"Download Excel": "下載 Excel",
"Download this view Excel": "下載 Excel(已選)",
"Download period Excel": "下載 Excel(完整)",
"Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。",
"Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。",
"Filtered": "篩選",
"Breakdown": "分項",
"All floors": "全部樓層",
"FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{date}}",
"Generating...": "生成中...",
"Download report (PDF)": "下載報告 (PDF)",
"Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?",
"Failed to download Workbench truck routing summary. Please try again later.": "下載 Workbench 送貨路線摘要失敗,請稍後再試。"
}

+ 2
- 0
src/i18n/zh/navigation.json Parādīt failu

@@ -81,6 +81,8 @@
"nav.settings.items": "物品",
"nav.settings.itemDefaultShelfLife": "物品預設保質期",
"nav.settings.masterDataIssues": "BOM / 物料單位問題",
"nav.settings.stockLedgerFix": "庫存帳修復",
"nav.breadcrumb.stockLedgerFix": "庫存帳修復",
"nav.settings.priceInquiry": "價格查詢",
"nav.settings.printer": "列印機",
"nav.settings.qcCategory": "QC 品檢模板",


+ 99
- 2
src/i18n/zh/pickOrder.json Parādīt failu

@@ -568,5 +568,102 @@
"Lot status is unavailable. Cannot switch or bind; pick line was not updated.": "批號狀態為「不可用」,無法換批或綁定;揀貨行未更新。",
"No lot rows. Select a line in the table above.": "尚無批號資料。請在上方表格勾選一行提料單明細。",
"No stock out line for this lot": "此批號尚無出庫行,無法提交。",
"No pending pick line left for this item. It may already be completed or fully processed.": "此貨品已無待揀行,可能已完成或不在本單可掃範圍。"
}
"No pending pick line left for this item. It may already be completed or fully processed.": "此貨品已無待揀行,可能已完成或不在本單可掃範圍。",

"FG Carton Qty": "成品出倉出箱數量",
"成品出倉出箱數量": "成品出倉出箱數量",
"Truck Routing Summary": "送貨路線摘要",
"送貨路線摘要": "送貨路線摘要",
"車線-X": "車線-X",
"Lot QR Code": "批號 QR 碼",
" 批號 QR 碼": "批號 QR 碼",
"Cannot determine this lot status": "無法判斷此批號狀態",
"Lot status: expired": "此批號狀態:已過期",
"This pick line was rejected. Please scan another lot.": "此出庫行:已拒絕,請改掃其他批號",
"This pick line is already completed. No further pick needed.": "此出庫行:已完成,無需再提貨",
"No lot bound / no available stock line. Please scan nearby inbound or transfer QR.": "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR",
"Lot status: depleted (no remaining stock)": "此批號狀態:已用畢(無剩餘庫存)",
"Lot status: depleted (available qty is 0)": "此批號狀態:已用畢(可用量為 0)",
"Lot status: unavailable (not put away or line unavailable)": "此批號狀態:庫存不可用(未上架或行狀態不可用)",
"Lot status: ready to pick": "此批號狀態:可提貨",
"This lot ({{lot}}) was rejected and cannot be used. Please scan another lot.": "此批次({{lot}})已被拒絕,無法使用。請掃描其他批次。",
"No lot information for this item in the current order": "當前訂單中沒有此物品的批次資訊",
"Lot label print (pick station)": "批號標籤列印(提貨台)",
"Scan content": "掃碼內容",
"Scan then press Enter or click Look up": "掃描後按 Enter 或點「查詢」",
"Look up": "查詢",
"Clear": "清除",
"Printer": "印表機",
"Please select": "請選擇",
"Print copies": "列印張數",
"Refresh lot list": "刷新批號清單",
"Selected printer": "已選:{{printer}}",
"Item code name": "品號:{{code}} {{name}}",
"No available lots on this floor": "找不到該樓層有可用批號(availableQty > 0)。",
" (current lot)": "(當前批次)",
"Location with value": "位置:{{location}}",
"Available qty with uom": "可用量:{{qty}} 單位:{{uom}}",
"Print label": "列印標籤",
"Show QR": "顯示 QR",
"This row has no QR payload": "此列無法取得 QR payload(需 stockInLineId)",
"This pick line already scanned or completed, QR cannot be shown": "此出庫行已掃碼或已完成,無法顯示 QR",
"No lots available to print labels": "沒有任何批號可列印標籤",
"Failed to load printer list": "載入印表機清單失敗",
"Loaded available lots for this item": "已載入同品可用批號清單",
"Analysis failed": "分析失敗",
"Invalid itemId, cannot load lot list.": "無效 itemId,無法載入批號清單。",
"Invalid scan format. Please scan again.": "掃碼內容格式錯誤,請重新掃碼",
"Scan or look up once before refreshing the lot list.": "請先掃碼或查詢一次,才可刷新批號清單。",
"Print quantity must be an integer of 1 or more": "列印張數需為大於等於 1 的整數",
"Print sent: Lot {{lotNo}}": "已送出列印:Lot {{lotNo}}",
"Print failed": "列印失敗",
"Failed to load FG carton quantity. Please try again later.": "載入成品出倉出箱數量失敗,請稍後再試。",
"Cartons": "箱數",
"{{count}} cartons": "{{count}} 箱",
"No chart data": "沒有圖表資料",
"Total": "總數",
"2/F carton qty": "2/F 出箱數",
"4/F carton qty": "4/F 出箱數",
"Truck X carton qty": "車線-X 出箱數",
"Total carton qty": "總出箱數",
"Summary": "彙總",
"All floors": "全部樓層",
"Last 7 days": "最近7天",
"This month": "本月",
"This year": "本年",
"FG carton qty last 7 days title": "成品出倉出箱數量(最近7天)- {{floor}} - 基準日 {{date}}",
"FG carton qty this month title": "成品出倉出箱數量(本月)- {{floor}} - {{period}}",
"FG carton qty this year title": "成品出倉出箱數量(本年)- {{floor}} - {{period}}",
"Exporting...": "匯出中...",
"Download Excel": "下載 Excel",
"Download this view Excel": "下載 Excel(已選)",
"Download period Excel": "下載 Excel(完整)",
"Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。",
"Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。",
"Filtered": "篩選",
"Breakdown": "分項",
"FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{date}}",
"Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)",
"2/F or 4/F": "2/F 或 4/F",
"Lane": "車線",
"Shop Code": "店鋪組別",
"Shop code": "店鋪編號",
"Back to previous level": "返回上一層",
"All lanes": "全部車線",
"All shops": "全部店鋪組別",
"Cartons by lane": "按車線統計箱數",
"Cartons by shop": "按店鋪編號統計箱數",
"Cartons by shop group": "按店鋪組別統計箱數",
"Cartons by floor": "按樓層統計箱數",
"Share": "佔比",
"Click a row to filter": "點選列以篩選",
"Click a bar to filter": "點選長條以篩選",
"Reset filters": "重設篩選",
"Generating...": "生成中...",
"Download report (PDF)": "下載報告 (PDF)",
"Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?",
"Failed to download Workbench truck routing summary. Please try again later.": "下載 Workbench 送貨路線摘要失敗,請稍後再試。",
"Confirm print drafts": "確認列印 {{count}} 張草稿?",
"Floor": "樓層",
"All": "全部"
}

+ 147
- 0
src/i18n/zh/stockLedgerFix.json Parādīt failu

@@ -0,0 +1,147 @@
{
"pageTitle": "庫存帳修復",
"inventory10Title": "1.0 inventory(方案 A)",
"inventory10Description": "保留現有 id;lot 有、inventory 沒有的 UOM 會新增;onHand = Σ(in−out) 按 item+UOM。沒有 lot 的列會變成 0。同一料多顆舊列時只留一顆 stock UOM 桶,其餘 deleted=1。請在進出倉暫停時執行,再逐日 Fix。",
"inventory10LoadError": "無法載入 inventory 1.0 預覽(需要 ADMIN / TESTING)",
"inventory10Preview": "現有 inventory {{rows}} 列;lot 的 item+UOM {{lotPairs}} 組;缺 {{missing}} 組;stockUomId 仍 NULL {{nullUom}} 列。",
"inventory10Running": "重算中…",
"inventory10Run": "Run inventory 1.0 (A)",
"inventory10Confirm": "方案 A:保留現有 inventory.id;從 lot 回填 stockUomId(舊 uomId 是 stock 則改回 base);補缺 (itemId, stockUomId);用 lot 的 in−out 重算 onHand/unavailable。不寫 onHoldQty、不改 inventory_lot_line。確定執行?",
"inventory10Done": "inventory 1.0 完成:回填 stockUomId {{patched}} 列、新增 {{inserted}} 列、殘列 deleted {{orphans}}、重算 {{updated}} 列、之後仍缺 UOM {{missingAfter}}、仍 NULL stockUomId {{nullAfter}}",
"inventory10Fail": "inventory 1.0 失敗",
"adjTitle": "2.7 ADJ:超發 remain→0,再對齊 ledger",
"adjDescription": "請先選 ADJ 日期再預覽。預設昨天;freeze dump 夜可選今天(例如 31/8 23:17 寫 ADJ 到 31/8)。先處理超發(line 入少於出):把該 lot 的 inQty 補到等於 outQty(remain=0,trigger 重算 inventory)。再處理 miss:line 與 ledger 有差就 ADJ,掛既有 lot 的入/出倉行(不改 lot 數量)。一次 Apply 共用一張入倉單、一張出倉單,下面多行;同一 ADJ 日期再跑會沿用這兩張單。明細最多 20 列。",
"adjDateLabel": "ADJ 日期",
"adjDateRequired": "請填 ADJ 日期",
"adjDateFuture": "ADJ 日期不能是未來",
"adjDateAndPreviewRequired": "請填 ADJ 日期並先預覽",
"adjTodayFreeze": "今天(freeze 夜)",
"adjYesterday": "昨天",
"adjLoadError": "無法載入 ADJ 預覽(需要 ADMIN / TESTING)",
"adjPreviewSummary": "ADJ 日期 {{date}};可處理 {{lotCount}} 條 lot(超發 remain→0 {{overIssueCount}}、合計 {{sumOverIssue}};miss 入 {{adjInCount}}、出 {{adjOutCount}});缺入合計 {{sumMissIn}}、缺出合計 {{sumMissOut}};SKU 淨額 {{skuNet}}{{skuNetNote}}{{revNote}}。下列最多 20 列(超發優先)。",
"adjSkuNetNote": "(非 0 表示有缺入沒有對應缺出)",
"adjRevNote": ";含反向 ADJ {{count}} 條(ledger 多記)",
"adjColLotLineId": "lotLineId",
"adjColItemCode": "itemCode",
"adjColLineInOut": "line in/out",
"adjColLedgerInOut": "ledger in/out",
"adjColMissIn": "缺入",
"adjColMissOut": "缺出",
"adjColOverIssue": "超發",
"adjPreviewing": "預覽中…",
"adjPreviewAgain": "重新預覽",
"adjPreview": "預覽",
"adjApplying": "寫入 ADJ 中…",
"adjApply": "Apply ADJ",
"adjConfirm": "會在 {{date}} 先修超發 remain→0({{overIssueCount}} 條 lot,合計 {{sumOverIssue}},會改 lot inQty)。再補 miss ADJ:入向 {{adjInCount}} 筆、出向 {{adjOutCount}} 筆。入向全部掛同一張入倉單、出向全部掛同一張出倉單(miss 不改 lot 數量)。缺入合計 {{sumMissIn}}、缺出合計 {{sumMissOut}}。確定?",
"adjDone": "ADJ {{date}}:超發 remain→0 {{overIssuePatched}}、入 {{insertedIn}}、出 {{insertedOut}}、lotQty {{filledLotQty}}、balance {{filledBalance}}、日結 {{dayRowsWritten}}",
"adjFail": "ADJ 對齊失敗",
"tabCalendar": "日曆",
"tabInventory": "Inventory",
"tabLot": "Lot line",
"calendarHint": "點左上標題可先選年再選月。換月不會查資料;點某一天才檢查當天。今天可修(freeze dump 夜)。",
"selectDate": "請選日期",
"dayLoadError": "無法載入當日檢查",
"dayLedgerCount": "當天 ledger {{cnt}} 列",
"cannotFixFuture": "不能修未來日期",
"cannotFixFutureRange": "不能修未來日期(今天可以,給 freeze dump 夜修)",
"pickAtLeastOneStep": "請至少勾一個步驟(預設全跑 2.1–2.6)",
"allSteps216": "全部 2.1–2.6",
"confirmPartialSteps": "只跑 {{steps}}({{date}})。未勾的步驟不會重算。確定?",
"fixDayDone": "已修 {{date}}({{steps}}):lot {{lot}}、uom {{uom}}、inventory {{inventory}}、lotQty {{lotQty}}、balance {{balance}}、日結 {{dayRows}}",
"fixFailed": "修復失敗",
"fixing": "修復中…",
"fixThisDay": "Fix this day",
"stepsTitle": "步驟(預設全跑)",
"stepsHint": "套 SQL 後只改 inventoryId 時只勾 2.3。只跑部分時未勾的不會重算。",
"selectAll": "全選",
"step21": "2.1 lot",
"step22": "2.2 uom",
"step23": "2.3 inventoryId",
"step24": "2.4 lotQty",
"step25": "2.5 balance",
"step26": "2.6 日結",
"rangeTitle": "區間修(2.1–2.6 一次)",
"rangeHint": "使用上面勾的步驟,一次 API 但改為逐日(與單日修相同 SQL,每天 commit)。可含今天(freeze dump 夜)。前端最長約 20 分鐘;大區間仍建議匯出 SQL。",
"rangeFromRequired": "請填區間修 from / to",
"toMustBeGteFrom": "to 必須 ≥ from",
"fromTooEarly": "from 不能早於 {{date}}",
"rangeConfirm": "一次修 {{from}} → {{to}}(步驟:{{steps}})。改為逐日跑(每天 commit)。中途失敗時已跑完的天會留下。確定?",
"rangeProgress": "區間修 {{from}} → {{to}}({{steps}})進行中…",
"rangeDone": "區間修完成 {{date}}({{steps}}):lot {{lot}}、uom {{uom}}、inventory {{inventory}}、lotQty {{lotQty}}、balance {{balance}}、日結 {{dayRows}}",
"rangeFail": "區間修失敗",
"rangeRunning": "區間修中…",
"rangeRun": "區間修",
"from": "from",
"to": "to",
"exportTitle": "匯出已修 SQL(新庫用)",
"exportHint": "勾選要匯出的段落(同日修步驟)。套檔順序:1.0 → ledger(±2.3)→ 2.7 入出倉單+ADJ INSERT → 2.6。完整檔(含 1.0+2.3)且正式庫 freeze/id 同源時,可跳過正式庫 1.0/2.3。預設只匯 ledger+日結(舊行為)。",
"exportPickAtLeastOne": "請至少勾一個匯出項目",
"export23Without10": "已勾 2.3 inventoryId 但未勾 1.0 inventory:正式庫若缺新 id,UPDATE 會指到空列。建議一併勾 1.0(完整檔)。仍要匯出?",
"exportFail": "匯出失敗",
"exporting": "匯出中…",
"exportSql": "匯出 .sql",
"exportDefault": "預設",
"exportFull": "完整檔",
"exportFromToRequired": "請填 from / to",
"export10": "1.0 inventory",
"export23": "2.3 inventoryId",
"exportLedger": "ledger lot/uom/lotQty/balance",
"export26": "2.6 日結",
"export27": "2.7 ADJ+入出倉單",
"invTabHint": "搜 itemCode 或 inventoryId。Fix 會重算這顆料到今天為止全部流水的 lotQty、日結與 balance,不必逐日點日曆。白天倉還在寫這顆料時,修完後新流水仍由 live writer 接。",
"invSearchLabel": "itemCode / inventoryId",
"searching": "搜尋中…",
"search": "搜尋",
"searchFailed": "搜尋失敗",
"colInventoryId": "inventoryId",
"colItemCode": "itemCode",
"colUomId": "uomId",
"colLedgerRows": "ledger 列",
"invLoadError": "無法載入 inventory",
"invScopeSummary": "{{itemCode}} / inventory {{id}} / uom {{uomId}};{{firstDate}} → {{lastDate}};{{cnt}} 列;最後 balance {{lastBalance}}",
"invFixConfirm": "會重算這顆 inventory 到今天為止全部 ledger 的 lotQty、日結,以及這顆料的 balance。確定?",
"invFixDone": "已修 inventory {{id}}:lot {{lot}}、uom {{uom}}、inventory {{inventory}}、lotQty {{lotQty}}、balance {{balance}}、日結 {{dayRows}}",
"fixThisInventory": "Fix this inventory",
"lotTabHint": "搜 lotNo 或 inventoryLotLineId。Fix 只重算這張 lot 的 lotQty 與日結,不改 balance。",
"lotSearchLabel": "lotNo / lot line id",
"colLotLineId": "lot line id",
"colLotNo": "lotNo",
"lotLoadError": "無法載入 lot",
"lotScopeSummary": "{{lotNo}} / lot {{id}} / {{itemCode}};{{firstDate}} → {{lastDate}};{{cnt}} 列;最後 lotQtyAfter {{lastLotQtyAfter}}",
"lotFixConfirm": "只修這張 lot 的 lotQty 與日結,不會改整顆料的 balance。確定?",
"lotFixDone": "已修 lot {{id}}:lot {{lot}}、uom {{uom}}、inventory {{inventory}}、lotQty {{lotQty}}、日結 {{dayRows}}",
"fixThisLot": "Fix this lot",
"checkItem": "項目",
"checkStatus": "狀態",
"checkCorrect": "正確",
"checkMiss": "缺",
"checkIncorrect": "不正確",
"checkReason": "原因",
"checkRows": "列數",
"checkDash": "—",
"canAutoFix": "可自動修(按 Fix 會處理)",
"cannotAutoFix": "不能自動修(Fix 不會消;舊 TKE 開新批/真超發/沒有 lot 來源)",
"verdictCorrect": "correct",
"verdictMiss": "miss",
"verdictOverIssue": "over-issue",
"verdictCanFix": "可修",
"verdictCannotFix": "不能修",
"verdictIncorrect": "incorrect",
"part": {
"lot": "inventoryLotLineId",
"uom": "uomId",
"inventory": "inventoryId",
"lotQty": "lotQtyBefore/After(lot 已有)",
"overIssue": "over-issue (lotQty < 0)",
"balance": "balance",
"dayTable": "stock_lot_day",
"fixable21a": "可修 2.1a:SIL/SOL 已有 lot line",
"fixable21b": "可修 2.1b:inventoryLotId 僅一條 line",
"fixableLotQty": "可修:lot 已有但缺 lotQty",
"cannotNoSource": "不能修:沒有 SIL/SOL lot 來源",
"cannotMultiLine": "不能修:同一 lot 有多條 warehouse line",
"earlyTke": "不能修:舊 TKE 開新批(sibling 盤盈)",
"realOver": "不能修:over-issue 非舊 TKE(真超發/帳不一致)"
}
}

+ 28
- 10
src/utils/workbenchPickLotUtils.ts Parādīt failu

@@ -150,44 +150,62 @@ export function buildUnpickableScanRowPatch(
return patch;
}

export function getWorkbenchSourceLotStatusSummary(lot: WorkbenchPickLotLike | null | undefined): {
export function getWorkbenchSourceLotStatusSummary(
lot: WorkbenchPickLotLike | null | undefined,
t?: PickOrderT,
): {
severity: "success" | "warning" | "error";
text: string;
} {
const tr = (key: string) => (t ? t(key) : key);
if (!lot) {
return { severity: "warning", text: "無法判斷此批號狀態" };
return { severity: "warning", text: tr("Cannot determine this lot status") };
}
if (isWorkbenchSourceLotExpired(lot)) {
return { severity: "error", text: "此批號狀態:已過期" };
return { severity: "error", text: tr("Lot status: expired") };
}
const solSt = solStatusOf(lot);
if (solSt === "rejected") {
return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" };
return {
severity: "warning",
text: tr("This pick line was rejected. Please scan another lot."),
};
}
if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") {
return { severity: "warning", text: "此出庫行:已完成,無需再提貨" };
return {
severity: "warning",
text: tr("This pick line is already completed. No further pick needed."),
};
}
const isNoLotRow =
lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === "";
if (isNoLotRow) {
return {
severity: "warning",
text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR",
text: tr(
"No lot bound / no available stock line. Please scan nearby inbound or transfer QR.",
),
};
}
const av = String(lot.lotAvailability || "").toLowerCase();
if (av === "insufficient_stock") {
return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" };
return {
severity: "warning",
text: tr("Lot status: depleted (no remaining stock)"),
};
}
const avail = Number(lot.availableQty);
if (lot.lotNo && Number.isFinite(avail) && avail <= 0) {
return { severity: "warning", text: "此批號狀態:已用畢(可用量為 0)" };
return {
severity: "warning",
text: tr("Lot status: depleted (available qty is 0)"),
};
}
if (isInventoryLotLineUnavailable(lot)) {
return {
severity: "warning",
text: "此批號狀態:庫存不可用(未上架或行狀態不可用)",
text: tr("Lot status: unavailable (not put away or line unavailable)"),
};
}
return { severity: "success", text: "此批號狀態:可提貨" };
return { severity: "success", text: tr("Lot status: ready to pick") };
}

Notiek ielāde…
Atcelt
Saglabāt