Procházet zdrojové kódy

update translate, do finish jump page, fix jo expiry can not submit,

production
CANCERYS\kw093 před 2 dny
rodič
revize
6159a435b8
11 změnil soubory, kde provedl 159 přidání a 32 odebrání
  1. +7
    -1
      src/app/api/pickOrder/actions.ts
  2. +57
    -4
      src/components/DoWorkbench/DoWorkbenchTabs.tsx
  3. +24
    -4
      src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx
  4. +34
    -4
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  5. +3
    -3
      src/components/JoWorkbench/JoPickOrderList.tsx
  6. +25
    -12
      src/components/JoWorkbench/newJobPickExecution.tsx
  7. +1
    -1
      src/components/Jodetail/FinishedGoodSearchWrapper.tsx
  8. +1
    -1
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  9. +4
    -2
      src/i18n/zh/common.json
  10. +2
    -0
      src/i18n/zh/jo.json
  11. +1
    -0
      src/i18n/zh/pickOrder.json

+ 7
- 1
src/app/api/pickOrder/actions.ts Zobrazit soubor

@@ -381,6 +381,7 @@ export interface CompletedDoPickOrderSearchParams {
deliveryNoteCode?: string;
/** 卡車/車道(後端 truckLanceCode 模糊匹配) */
truckLanceCode?: string;
ticketNo?: string;
}
export interface PickExecutionIssue {
id: number;
@@ -713,6 +714,9 @@ export const fetchCompletedDoPickOrdersWorkbench = async (
if (searchParams?.truckLanceCode) {
params.append("truckLanceCode", searchParams.truckLanceCode);
}
if (searchParams?.ticketNo) {
params.append("ticketNo", searchParams.ticketNo);
}

const queryString = params.toString();
const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders-workbench/${userId}${
@@ -742,7 +746,9 @@ export const fetchCompletedDoPickOrdersWorkbenchAll = async (
if (searchParams?.truckLanceCode) {
params.append("truckLanceCode", searchParams.truckLanceCode);
}

if (searchParams?.ticketNo) {
params.append("ticketNo", searchParams.ticketNo);
}
const queryString = params.toString();
const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders-workbench-all${
queryString ? `?${queryString}` : ""


+ 57
- 4
src/components/DoWorkbench/DoWorkbenchTabs.tsx Zobrazit soubor

@@ -1,7 +1,8 @@
"use client";

import { Autocomplete, Box, Tab, Tabs, TextField, Typography } from "@mui/material";
import React from "react";
import { Autocomplete, Box, CircularProgress, Tab, Tabs, TextField, Typography } from "@mui/material";
import React, { Suspense } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import DoWorkbenchPickShell from "./DoWorkbenchPickShell";
import type { PrinterCombo } from "@/app/api/settings/printer";
import GoodPickExecutionWorkbenchRecord from "./GoodPickExecutionWorkbenchRecord";
@@ -14,6 +15,9 @@ import { fetchWorkbenchReleasedDoPickOrdersForSelectionToday } from "@/app/api/d
import { Button } from "@mui/material";
import FinishedGoodCartonDashboardTab from "../FinishedGoodSearch/FinishedGoodCartonDashboardTab";
import TruckRoutingSummaryTabWorkbench from "./TruckRoutingSummaryTabWorkbench";

const ALLOWED_WORKBENCH_TABS = new Set([0, 1, 2, 3, 5, 6]);

type Props = {
defaultTabIndex?: 0 | 1;
printerCombo?: PrinterCombo[];
@@ -25,7 +29,18 @@ function TabPanel(props: { value: number; index: number; children: React.ReactNo
return <Box sx={{ pt: 2 }}>{children}</Box>;
}

const DoWorkbenchTabs: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo = [] }) => {
const DoWorkbenchTabsInner: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo = [] }) => {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();

const urlTabStr = searchParams.get("tab");
const urlTicketRaw = searchParams.get("ticketNo");
const urlTicketNo =
urlTicketRaw && urlTicketRaw.trim() !== ""
? decodeURIComponent(urlTicketRaw.trim())
: null;

const [tab, setTab] = React.useState<number>(defaultTabIndex);
const [a4Printer, setA4Printer] = React.useState<PrinterCombo | null>(null);
const [labelPrinter, setLabelPrinter] = React.useState<PrinterCombo | null>(null);
@@ -54,6 +69,28 @@ const DoWorkbenchTabs: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo =
void fetchReleasedOrderCount();
}, [fetchReleasedOrderCount]);

React.useEffect(() => {
if (urlTabStr == null || urlTabStr === "") return;
const n = parseInt(urlTabStr, 10);
if (!Number.isNaN(n) && ALLOWED_WORKBENCH_TABS.has(n)) {
setTab(n);
}
}, [urlTabStr]);

const handleTabChange = React.useCallback(
(_: React.SyntheticEvent, newTab: number) => {
setTab(newTab);
const params = new URLSearchParams(searchParams.toString());
params.set("tab", String(newTab));
if (newTab !== 1) {
params.delete("ticketNo");
}
const qs = params.toString();
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
},
[pathname, router, searchParams],
);

const handleAllDraft = React.useCallback(async () => {
try {
if (!a4Printer) {
@@ -186,7 +223,7 @@ const DoWorkbenchTabs: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo =
{`${t("Print All Draft")} (${releasedOrderCount})`}
</Button>
</Stack>
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={tab} onChange={handleTabChange} sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tab label={t("Pick Order Detail")} value={0} />
<Tab label={t("Finished Good Record")} value={1} />
<Tab label={t("Finished Good Record (All)")} value={2} />
@@ -200,18 +237,22 @@ const DoWorkbenchTabs: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo =
</TabPanel>
<TabPanel value={tab} index={1}>
<GoodPickExecutionWorkbenchRecord
key={`workbench-record-mine-${urlTicketNo ?? ""}`}
printerCombo={printerCombo}
listScope="mine"
a4Printer={a4Printer}
labelPrinter={labelPrinter}
initialTicketNo={urlTicketNo}
/>
</TabPanel>
<TabPanel value={tab} index={2}>
<GoodPickExecutionWorkbenchRecord
//key={`workbench-record-all-${urlTicketNo ?? ""}`}
printerCombo={printerCombo}
listScope="all"
a4Printer={a4Printer}
labelPrinter={labelPrinter}
//initialTicketNo={urlTicketNo}
/>
</TabPanel>
<TabPanel value={tab} index={3}>
@@ -228,5 +269,17 @@ const DoWorkbenchTabs: React.FC<Props> = ({ defaultTabIndex = 0, printerCombo =
);
};

const DoWorkbenchTabs: React.FC<Props> = (props) => (
<Suspense
fallback={
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
}
>
<DoWorkbenchTabsInner {...props} />
</Suspense>
);

export default DoWorkbenchTabs;


+ 24
- 4
src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx Zobrazit soubor

@@ -44,6 +44,7 @@ type Props = {
listScope?: "mine" | "all";
a4Printer: PrinterCombo | null;
labelPrinter: PrinterCombo | null;
initialTicketNo?: string | null;
};

const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
@@ -51,6 +52,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
listScope = "mine",
a4Printer,
labelPrinter,
initialTicketNo,
}) => {
const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -70,6 +72,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
shopName?: string;
deliveryNoteCode?: string;
truckLanceCode?: string;
ticketNo?: string;
}) => {
setLoading(true);
try {
@@ -89,8 +92,13 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
}, [currentUserId, listScope]);

useEffect(() => {
void loadData({ targetDate: dayjs().format("YYYY-MM-DD") });
}, [loadData]);
const today = dayjs().format("YYYY-MM-DD");
const tn = initialTicketNo?.trim() || undefined;
void loadData({
targetDate: today,
...(tn ? { ticketNo: tn } : {}),
});
}, [loadData, initialTicketNo]);

const searchCriteria: Criterion<any>[] = useMemo(
() => [
@@ -115,8 +123,16 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
type: "date",
defaultValue: dayjs().format("YYYY-MM-DD"),
},
{
label: t("Ticket No"),
paramName: "ticketNo",
type: "text",
...(initialTicketNo?.trim()
? { preFilledValue: initialTicketNo.trim() }
: {}),
},
],
[t],
[t, initialTicketNo],
);

const handleSearch = useCallback((query: Record<string, any>) => {
@@ -126,6 +142,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
shopName: query.shopName || undefined,
deliveryNoteCode: query.deliveryNoteCode || undefined,
truckLanceCode: query.truckLanceCode || undefined,
ticketNo: query.ticketNo || undefined,
});
}, [loadData]);

@@ -582,10 +599,10 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
<Box>
<Box sx={{ mb: 2 }}>
<SearchBox
key={`workbench-search-${listScope}-${initialTicketNo ?? ""}`}
criteria={searchCriteria}
onSearch={handleSearch}
onReset={handleSearchReset}
// searchQuery={searchQuery}
/>
</Box>
<Stack
@@ -617,6 +634,9 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="h6">{row.deliveryNoteCode || "-"}</Typography>
<Typography variant="body2" color="text.secondary">
{row.ticketNo || "-"}
</Typography>
<Typography variant="body2" color="text.secondary">
{row.shopName}
</Typography>


+ 34
- 4
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Zobrazit soubor

@@ -25,7 +25,7 @@ import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodePro
import { fetchLotDetail } from "@/app/api/inventory/actions";
import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import {
updateStockOutLineStatus,
createStockOutLine,
@@ -383,6 +383,7 @@ const WorkbenchGoodPickExecutionDetail: React.FC<Props> = ({
const workbenchMode = true;
const { t } = useTranslation("pickOrder");
const router = useRouter();
const pathname = usePathname();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
@@ -519,6 +520,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const autoAssignRef = useRef(false);
/** 曾成功載入過 workbench 階層資料;避免「列表仍有單但階層暫空」時對外層重複觸發造成迴圈 */
const workbenchHierarchicalReadyRef = useRef(false);
/** 最後一筆 workbench 票號(階層清空或完成後仍可用於導向完成紀錄) */
const lastWorkbenchTicketNoRef = useRef<string | null>(null);
/** 同一筆揀貨完成後只導向「完成紀錄」分頁一次 */
const workbenchFinishNavigateDoneRef = useRef(false);

const formProps = useForm();
const errors = formProps.formState.errors;
@@ -718,15 +723,31 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
// 检查数据结构
if (!hierarchicalData?.fgInfo || !hierarchicalData.pickOrders?.length) {
console.warn("⚠️ No FG info or pick orders found");
const hadWorkbenchData = workbenchHierarchicalReadyRef.current;
const ticketForRedirect =
String(hierarchicalData?.fgInfo?.ticketNo ?? "").trim() ||
lastWorkbenchTicketNoRef.current ||
"";
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
setIssuePickedQtyBySolId({});
setFgPickOrders([]);
if (workbenchHierarchicalReadyRef.current) {
workbenchHierarchicalReadyRef.current = false;
workbenchHierarchicalReadyRef.current = false;
if (hadWorkbenchData) {
onWorkbenchHierarchyEmpty?.();
}
if (
hadWorkbenchData &&
ticketForRedirect &&
!workbenchFinishNavigateDoneRef.current
) {
workbenchFinishNavigateDoneRef.current = true;
router.replace(
`${pathname}?tab=1&ticketNo=${encodeURIComponent(ticketForRedirect)}`,
{ scroll: false },
);
}
return;
}
@@ -779,6 +800,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setFgPickOrders([fgOrder]);
workbenchHierarchicalReadyRef.current = true;
lastWorkbenchTicketNoRef.current =
String(fgOrder.ticketNo ?? "").trim() || null;
workbenchFinishNavigateDoneRef.current = false;
console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder);
console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
@@ -967,7 +991,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
} finally {
setCombinedDataLoading(false);
}
}, [currentUserId, checkAllLotsCompleted, onWorkbenchHierarchyEmpty]); // 移除 selectedPickOrderId 依赖
}, [
currentUserId,
checkAllLotsCompleted,
onWorkbenchHierarchyEmpty,
router,
pathname,
]); // 移除 selectedPickOrderId 依赖

/** After workbench scan-pick (incl. split → new stock_out_line), reload hierarchical rows. */
const refreshWorkbenchAfterScanPick = useCallback(async () => {


+ 3
- 3
src/components/JoWorkbench/JoPickOrderList.tsx Zobrazit soubor

@@ -45,7 +45,7 @@ const JoPickOrderList: React.FC<Props> = () => {
paramName: "BOM Description",
type: "select-labelled",
options: [
{ label: t("All"), value: "All" },
//{ label: t("All"), value: "All" },
{ label: t("FG"), value: "FG" },
{ label: t("WIP"), value: "WIP" },
],
@@ -56,7 +56,7 @@ const JoPickOrderList: React.FC<Props> = () => {
paramName: "bomType",
type: "select-labelled",
options: [
{ label: t("All"), value: "All" },
//{ label: t("All"), value: "All" },
{ label: t("Drink"), value: "drink" },
{ label: t("Powder Mixture"), value: "Powder_Mixture" },
{ label: t("Other"), value: "other" },
@@ -67,7 +67,7 @@ const JoPickOrderList: React.FC<Props> = () => {
paramName: "floor",
type: "select-labelled",
options: [
{ label: t("All"), value: "ALL" },
//{ label: t("All"), value: "ALL" },
{ label: "2F", value: "2F" },
{ label: "3F", value: "3F" },
{ label: "4F", value: "4F" },


+ 25
- 12
src/components/JoWorkbench/newJobPickExecution.tsx Zobrazit soubor

@@ -3002,6 +3002,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const canPostScanPick =
// unavailable lot: Just Completed must always submit qty=0, even without lotNo
isUnavailableForJustComplete ||
isLotAvailabilityExpired(canonicalLotForSol) ||
// noLot row: Just Completed always submit qty=0
isNoLotForJustComplete ||
(canonicalLotForSol.lotNo &&
@@ -3014,6 +3015,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
if (canPostScanPick) {
const qtyToSend =
isUnavailableForJustComplete
? 0
: isLotAvailabilityExpired(canonicalLotForSol)
? 0
: isNoLotForJustComplete
? 0
@@ -3060,7 +3063,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
);
return;
}
const justCompleteErr = tPick(
const justCompleteErr = t(
"Just Completed (workbench): requires valid quantity; expired rows must not use this button.",
);
if (solId > 0) {
@@ -3964,7 +3967,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
!Number.isNaN(Number(fromPickRow))
? Number(fromPickRow)
: lockedSubmitQtyDisplay;

const totalAvail = Number(lot.itemTotalAvailableQty ?? 0);
const isLastLotUnavailable = Number.isFinite(totalAvail) && totalAvail === 0;
return (
<TableRow
key={`${lot.pickOrderLineId}-${lot.lotId}`}
@@ -3982,14 +3986,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
</Typography>
</TableCell>
<TableCell>
{row.isGroupFirst ? (
<>
{lot.itemCode} <br />
{lot.itemName} <br />
{lot.uomDesc}
</>
) : ""}
</TableCell>
{row.isGroupFirst ? (
<>
{lot.itemCode} <br />
{lot.itemName} <br />
{lot.uomDesc}
</>
) : ""}
</TableCell>

<TableCell>
<Typography variant="body2">
@@ -4043,7 +4047,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
{t(
"is expired. Please check around have available QR code or not.",
)}
</Box>
{isLastLotUnavailable && (
<Box
component="span"
sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}
>
{t("This is last lot, so no available lot.")}
</Box>
)}
</>
) : isInventoryLotLineUnavailable(lot) &&
!(
@@ -4093,7 +4106,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
openWorkbenchLotLabelModalForLot(lot)
}
disabled={
lot.lotAvailability === "expired" ||
(Number(lot.stockOutLineId) > 0 &&
actionBusyBySolId[
Number(lot.stockOutLineId)
@@ -4457,7 +4470,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
"partially_completed" ||
lot.stockOutLineStatus ===
"partially_complete" ||
isUnavailableLot ||
// isUnavailableLot ||
(Number(lot.stockOutLineId) > 0 &&
issuePickedQtyBySolId[
Number(lot.stockOutLineId)


+ 1
- 1
src/components/Jodetail/FinishedGoodSearchWrapper.tsx Zobrazit soubor

@@ -23,7 +23,7 @@ const JodetailSearchWrapper: React.FC & SubComponents = async () => {
*/
fetchPrinterCombo(),
]);
console.log("%c printerCombo:", "color:green", printerCombo);
//console.log("%c printerCombo:", "color:green", printerCombo);

return <JodetailSearch printerCombo={printerCombo} />;
};


+ 1
- 1
src/components/ProductionProcess/ProductionProcessDetail.tsx Zobrazit soubor

@@ -770,7 +770,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
) : isPaused ? (
<Chip label={t("Paused")} color="warning" size="small" />
) : isPass ? (
<Chip label={t("Pass")} color="success" size="small" />
<Chip label={t("Just Pass")} color="success" size="small" />
) : (
<Chip label={t("Unknown")} color="error" size="small" />
)


+ 4
- 2
src/i18n/zh/common.json Zobrazit soubor

@@ -284,8 +284,9 @@
"Finished Good Management": "成品出倉管理",
"提料順序": "提料順序",
"Filter": "過濾",
"Item Code": "材料編號",
"Item Name": "材料名稱",
"Item Code": "物料編號",
"Item Name": "物料名稱",
"Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "工單對料:需要有效數量;過期項目不能使用此按鈕。",
"Search & Jump": "搜尋並跳轉",
"Enter to jump to item": "按 Enter 直接跳到品項位置",
"Jump": "跳轉",
@@ -533,6 +534,7 @@
"Edit departure time": "編輯出發時間",
"Failed to load truck lane detail": "載入車線詳情失敗",
"Shop Detail": "店鋪詳情",
"Just Pass": "已完成",
"Truck Lane Detail": "車線詳情",
"Filter by Status": "按狀態篩選",
"All": "全部",


+ 2
- 0
src/i18n/zh/jo.json Zobrazit soubor

@@ -151,6 +151,8 @@
"Issue": "問題",
"Location": "位置",
"Scan Result": "掃碼結果",
"Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "工單對料:需要有效數量;過期項目不能使用此按鈕。",
"This is last lot, so no available lot.": "這是最後一個批次,所以沒有可用批次。",
"Expiry Date": "有效期",
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數",


+ 1
- 0
src/i18n/zh/pickOrder.json Zobrazit soubor

@@ -501,6 +501,7 @@
"label Printer" : "標籤打印機",
"A4 Printer" : "A4 打印機",
"Loading Sequence": "裝載序",
"Ticket No": "提票號碼",
"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.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。",
"Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。",


Načítá se…
Zrušit
Uložit