diff --git a/src/app/(main)/finishedGood/management/page.tsx b/src/app/(main)/finishedGood/management/page.tsx
new file mode 100644
index 0000000..1594c8f
--- /dev/null
+++ b/src/app/(main)/finishedGood/management/page.tsx
@@ -0,0 +1,30 @@
+import { I18nProvider } from "@/i18n";
+import { Metadata } from "next";
+import { Suspense } from "react";
+import FinishedGoodManagement from "@/components/FinishedGoodManagement";
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import { authOptions } from "@/config/authConfig";
+import { AUTH } from "@/authorities";
+
+export const metadata: Metadata = {
+ title: "Finished Good Management",
+};
+
+const Page = async () => {
+ const session = await getServerSession(authOptions);
+ const abilities = session?.user?.abilities ?? [];
+ if (!abilities.includes(AUTH.ADMIN)) {
+ redirect("/dashboard");
+ }
+ return (
+
+ }>
+
+
+
+ );
+};
+
+export default Page;
+
diff --git a/src/app/api/settings/item/itemOrderActions.ts b/src/app/api/settings/item/itemOrderActions.ts
new file mode 100644
index 0000000..9aeb4e9
--- /dev/null
+++ b/src/app/api/settings/item/itemOrderActions.ts
@@ -0,0 +1,59 @@
+"use server";
+
+import { BASE_API_URL } from "@/config/api";
+import { serverFetchJson } from "@/app/utils/fetchUtil";
+import { revalidateTag } from "next/cache";
+import { cache } from "react";
+
+export interface ItemOrderItem {
+ id: number;
+ code: string;
+ name: string;
+ itemOrder: number | null;
+ storeId?: string | null;
+ warehouse?: string | null;
+ area?: string | null;
+ slot?: string | null;
+ locationCode?: string | null;
+}
+
+export const fetchOrderedItemOrderItems = cache(async (search?: string): Promise => {
+ const params = new URLSearchParams();
+ if (search?.trim()) params.set("search", search.trim());
+ // Optional filtering by item type (e.g. fg)
+ const url = `${BASE_API_URL}/items/itemOrder/ordered${params.toString() ? `?${params.toString()}` : ""}`;
+ const res = await serverFetchJson(url, { method: "GET", next: { tags: ["itemOrder"] } });
+ return res ?? [];
+});
+
+export const fetchUnorderedItemOrderItems = cache(async (search?: string, type?: string): Promise => {
+ const params = new URLSearchParams();
+ if (search?.trim()) params.set("search", search.trim());
+ if (type?.trim()) params.set("type", type.trim());
+ const url = `${BASE_API_URL}/items/itemOrder/unordered${params.toString() ? `?${params.toString()}` : ""}`;
+ const res = await serverFetchJson(url, { method: "GET", next: { tags: ["itemOrder"] } });
+ return res ?? [];
+});
+
+export const searchUnorderedItemOrderItems = cache(
+ async (search: string, type?: string, limit: number = 200): Promise => {
+ const params = new URLSearchParams();
+ params.set("search", search.trim());
+ if (type?.trim()) params.set("type", type.trim());
+ params.set("limit", String(limit));
+ const url = `${BASE_API_URL}/items/itemOrder/unordered/search?${params.toString()}`;
+ const res = await serverFetchJson(url, { method: "GET", next: { tags: ["itemOrder"] } });
+ return res ?? [];
+ },
+);
+
+export const saveItemOrder = async (orderedItemIds: number[]) => {
+ const res = await serverFetchJson(`${BASE_API_URL}/items/itemOrder/save`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ orderedItemIds }),
+ });
+ revalidateTag("itemOrder");
+ return res;
+};
+
diff --git a/src/components/FinishedGoodManagement/FinishedGoodManagementPage.tsx b/src/components/FinishedGoodManagement/FinishedGoodManagementPage.tsx
new file mode 100644
index 0000000..3e9b57d
--- /dev/null
+++ b/src/components/FinishedGoodManagement/FinishedGoodManagementPage.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { Box, Tab, Tabs, Typography } from "@mui/material";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import PickOrderSequenceTab from "./PickOrderSequenceTab";
+
+export default function FinishedGoodManagementPage() {
+ const { t } = useTranslation("common");
+ const [tab, setTab] = React.useState(0);
+
+ return (
+
+
+
+ {t("Finished Good Management")}
+
+
+
+
+ setTab(v)} variant="scrollable">
+
+
+
+
+
+ {tab === 0 && }
+
+
+ );
+}
+
diff --git a/src/components/FinishedGoodManagement/FinishedGoodManagementWrapper.tsx b/src/components/FinishedGoodManagement/FinishedGoodManagementWrapper.tsx
new file mode 100644
index 0000000..2b7445f
--- /dev/null
+++ b/src/components/FinishedGoodManagement/FinishedGoodManagementWrapper.tsx
@@ -0,0 +1,15 @@
+import GeneralLoading from "../General/GeneralLoading";
+import FinishedGoodManagementPage from "./FinishedGoodManagementPage";
+
+interface SubComponents {
+ Loading: typeof GeneralLoading;
+}
+
+const FinishedGoodManagementWrapper: React.FC & SubComponents = async () => {
+ return ;
+};
+
+FinishedGoodManagementWrapper.Loading = GeneralLoading;
+
+export default FinishedGoodManagementWrapper;
+
diff --git a/src/components/FinishedGoodManagement/PickOrderSequenceTab.tsx b/src/components/FinishedGoodManagement/PickOrderSequenceTab.tsx
new file mode 100644
index 0000000..f68191c
--- /dev/null
+++ b/src/components/FinishedGoodManagement/PickOrderSequenceTab.tsx
@@ -0,0 +1,718 @@
+"use client";
+
+import React from "react";
+import {
+ Box,
+ Button,
+ Checkbox,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ IconButton,
+ Radio,
+ RadioGroup,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+} from "@mui/material";
+import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
+import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp";
+import { useTranslation } from "react-i18next";
+import { GridRowSelectionModel } from "@mui/x-data-grid";
+import {
+ fetchOrderedItemOrderItems,
+ fetchUnorderedItemOrderItems,
+ ItemOrderItem,
+ searchUnorderedItemOrderItems,
+ saveItemOrder,
+} from "@/app/api/settings/item/itemOrderActions";
+
+type Row = {
+ id: number;
+ code: string;
+ name: string;
+ itemOrder: number;
+};
+
+function normalize(s: string) {
+ return s.trim().toLowerCase();
+}
+
+function onlyDigits(s: string) {
+ return /^\d+$/.test(s);
+}
+
+function normalizeCode(s: string) {
+ return normalize(s).replace(/[^a-z0-9]/g, "");
+}
+
+function codeMatches(code: string, q: string) {
+ const query = normalizeCode(q);
+ if (!query) return true;
+ const c = normalizeCode(code);
+ if (c.includes(query)) return true;
+ // allow searching by trailing digits: "1074" matches "pp1074"
+ if (onlyDigits(query)) {
+ const digits = c.replace(/^[a-z]+/, "");
+ return digits.includes(query);
+ }
+ return false;
+}
+
+function toRows(items: ItemOrderItem[]): Row[] {
+ return items
+ .filter((x) => x.id != null)
+ .map((x, idx) => ({
+ id: x.id,
+ code: x.code ?? "",
+ name: x.name ?? "",
+ itemOrder: typeof x.itemOrder === "number" ? x.itemOrder : idx + 1,
+ }))
+ .sort((a, b) => a.itemOrder - b.itemOrder);
+}
+
+function reorderByIndex(arr: T[], from: number, to: number) {
+ const next = arr.slice();
+ const [item] = next.splice(from, 1);
+ next.splice(to, 0, item);
+ return next;
+}
+
+export default function PickOrderSequenceTab() {
+ const { t } = useTranslation("common");
+ const rowRefs = React.useRef(new Map());
+
+ const [loading, setLoading] = React.useState(false);
+ const [saving, setSaving] = React.useState(false);
+ const [rows, setRows] = React.useState([]);
+ const [originalIds, setOriginalIds] = React.useState([]);
+ const [selectionModel, setSelectionModel] = React.useState([]);
+
+ const [filterCode, setFilterCode] = React.useState("");
+ const [filterName, setFilterName] = React.useState("");
+ const [jumpQuery, setJumpQuery] = React.useState("");
+
+ const [addOpen, setAddOpen] = React.useState(false);
+ const [addSearch, setAddSearch] = React.useState("");
+ const [addLoading, setAddLoading] = React.useState(false);
+ const [addRows, setAddRows] = React.useState([]);
+ const [addSelection, setAddSelection] = React.useState([]);
+ const [insertAt, setInsertAt] = React.useState("");
+ const [insertPlacement, setInsertPlacement] = React.useState<"before" | "after">("before");
+
+ const [moveDialogOpen, setMoveDialogOpen] = React.useState(false);
+ const [moveRowId, setMoveRowId] = React.useState(null);
+ const [moveToOrder, setMoveToOrder] = React.useState("");
+ const [movePlacement, setMovePlacement] = React.useState<"before" | "after">("before");
+
+ const hasUnsavedChanges = React.useMemo(() => {
+ const ids = rows.map((r) => r.id);
+ if (ids.length !== originalIds.length) return true;
+ for (let i = 0; i < ids.length; i++) if (ids[i] !== originalIds[i]) return true;
+ return false;
+ }, [rows, originalIds]);
+
+ const refresh = React.useCallback(async () => {
+ setLoading(true);
+ try {
+ const items = await fetchOrderedItemOrderItems();
+ const r = toRows(items);
+ setRows(r.map((x, i) => ({ ...x, itemOrder: i + 1 })));
+ setOriginalIds(r.map((x) => x.id));
+ setSelectionModel([]);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ const filteredRows = React.useMemo(() => {
+ const qcRaw = filterCode.trim();
+ const qn = normalize(filterName);
+ const qcNorm = normalizeCode(qcRaw);
+ if (!qcNorm && !qn) return rows;
+ return rows.filter((r) => {
+ const okCode = !qcNorm || codeMatches(r.code, qcRaw);
+ const okName = !qn || normalize(r.name).includes(qn);
+ return okCode && okName;
+ });
+ }, [filterCode, filterName, rows]);
+
+ const MainFooter = React.useMemo(() => {
+ const Footer = () => {
+ const total = filteredRows.length;
+ const from = total === 0 ? 0 : 1;
+ const to = total;
+ return (
+
+ {`${from} ~ ${to} / ${total}`}
+
+ );
+ };
+ return Footer;
+ }, [filteredRows.length]);
+
+ const AddFooter = React.useMemo(() => {
+ const Footer = () => {
+ const total = addRows.length;
+ const from = total === 0 ? 0 : 1;
+ const to = total;
+ return (
+
+ {`${from} ~ ${to} / ${total}`}
+
+ );
+ };
+ return Footer;
+ }, [addRows.length]);
+
+ const selectedId = (selectionModel?.[0] as number | undefined) ?? undefined;
+ const selectedIndex = selectedId != null ? rows.findIndex((r) => r.id === selectedId) : -1;
+
+ const moveRowByIdTo = React.useCallback(
+ (rowId: number, toIndex: number) => {
+ const from = rows.findIndex((r) => r.id === rowId);
+ if (from < 0) return;
+ const to = Math.max(0, Math.min(rows.length - 1, toIndex));
+ if (to === from) return;
+ const next = reorderByIndex(rows, from, to).map((r, i) => ({ ...r, itemOrder: i + 1 }));
+ setRows(next);
+ const newId = next[to]?.id;
+ if (newId != null) setSelectionModel([newId]);
+ queueMicrotask(() => rowRefs.current.get(newId)?.scrollIntoView({ block: "center" }));
+ },
+ [rows],
+ );
+
+ const moveSelected = React.useCallback(
+ (delta: number) => {
+ if (selectedIndex < 0) return;
+ const to = Math.max(0, Math.min(rows.length - 1, selectedIndex + delta));
+ if (to === selectedIndex) return;
+ const next = reorderByIndex(rows, selectedIndex, to).map((r, i) => ({ ...r, itemOrder: i + 1 }));
+ setRows(next);
+ const newId = next[to]?.id;
+ if (newId != null) setSelectionModel([newId]);
+ queueMicrotask(() => rowRefs.current.get(newId)?.scrollIntoView({ block: "center" }));
+ },
+ [rows, selectedIndex],
+ );
+
+ const moveSelectedTo = React.useCallback(
+ (toIndex: number) => {
+ if (selectedIndex < 0) return;
+ const to = Math.max(0, Math.min(rows.length - 1, toIndex));
+ if (to === selectedIndex) return;
+ const next = reorderByIndex(rows, selectedIndex, to).map((r, i) => ({ ...r, itemOrder: i + 1 }));
+ setRows(next);
+ const newId = next[to]?.id;
+ if (newId != null) setSelectionModel([newId]);
+ queueMicrotask(() => rowRefs.current.get(newId)?.scrollIntoView({ block: "center" }));
+ },
+ [rows, selectedIndex],
+ );
+
+ const jumpTo = React.useCallback(() => {
+ const qRaw = jumpQuery.trim();
+ const q = normalize(qRaw);
+ if (!q) return;
+ const idx = rows.findIndex((r) => codeMatches(r.code, qRaw) || normalize(r.name).includes(q));
+ if (idx < 0) return;
+ const id = rows[idx].id;
+ setSelectionModel([id]);
+ queueMicrotask(() => rowRefs.current.get(id)?.scrollIntoView({ block: "center" }));
+ }, [jumpQuery, rows]);
+
+ const openAdd = React.useCallback(async () => {
+ setAddOpen(true);
+ setAddSearch("");
+ setAddSelection([]);
+ setInsertAt(selectedIndex >= 0 ? selectedIndex + 2 : rows.length + 1);
+ setAddLoading(true);
+ try {
+ // Do not fetch everything by default; wait for user search
+ setAddRows([]);
+ } finally {
+ setAddLoading(false);
+ }
+ }, [rows.length, selectedIndex]);
+
+ const searchAdd = React.useCallback(async () => {
+ const q = addSearch.trim();
+ if (!q) {
+ setAddRows([]);
+ setAddSelection([]);
+ return;
+ }
+ setAddLoading(true);
+ try {
+ // Search across ALL items without order (not only FG), with server-side LIMIT for performance
+ const items = await searchUnorderedItemOrderItems(q, undefined, 200);
+ setAddRows(toRows(items).map((x, i) => ({ ...x, itemOrder: i + 1 })));
+ setAddSelection([]);
+ } finally {
+ setAddLoading(false);
+ }
+ }, [addSearch]);
+
+ const confirmAdd = React.useCallback(() => {
+ const selectedIds = new Set(addSelection as number[]);
+ const picked = addRows.filter((r) => selectedIds.has(r.id));
+ if (picked.length === 0) {
+ setAddOpen(false);
+ return;
+ }
+ const existing = new Set(rows.map((r) => r.id));
+ const appended = picked.filter((r) => !existing.has(r.id));
+ const base = rows.slice();
+ const idx0 =
+ typeof insertAt === "number" && Number.isFinite(insertAt) ? Math.max(1, Math.floor(insertAt)) - 1 : base.length;
+ const insertIndexRaw = insertPlacement === "after" ? idx0 + 1 : idx0;
+ const insertIndex = Math.max(0, Math.min(base.length, insertIndexRaw));
+ const next = [...base.slice(0, insertIndex), ...appended, ...base.slice(insertIndex)].map((r, i) => ({
+ ...r,
+ itemOrder: i + 1,
+ }));
+ setRows(next);
+ setAddOpen(false);
+ }, [addRows, addSelection, insertAt, insertPlacement, rows]);
+
+ const openMoveDialog = React.useCallback(
+ (rowId: number) => {
+ const row = rows.find((r) => r.id === rowId);
+ setMoveRowId(rowId);
+ setMoveToOrder(row?.itemOrder ?? "");
+ setMovePlacement("before");
+ setMoveDialogOpen(true);
+ },
+ [rows],
+ );
+
+ const confirmMoveDialog = React.useCallback(() => {
+ if (moveRowId == null) return setMoveDialogOpen(false);
+ if (moveToOrder === "") return;
+ const n = Number(moveToOrder);
+ if (!Number.isFinite(n) || n < 1) return;
+ const baseIndex = Math.floor(n) - 1;
+ const toIndex = movePlacement === "after" ? baseIndex + 1 : baseIndex;
+ moveRowByIdTo(moveRowId, toIndex);
+ setMoveDialogOpen(false);
+ }, [movePlacement, moveRowByIdTo, moveRowId, moveToOrder]);
+
+ const onSave = React.useCallback(async () => {
+ setSaving(true);
+ try {
+ await saveItemOrder(rows.map((r) => r.id));
+ await refresh();
+ } finally {
+ setSaving(false);
+ }
+ }, [refresh, rows]);
+
+ const isAllAddSelected = React.useMemo(() => {
+ if (addRows.length === 0) return false;
+ return (addSelection as number[]).length === addRows.length;
+ }, [addRows.length, addSelection]);
+
+ const insertAtNumber = React.useMemo(() => {
+ if (insertAt === "") return null;
+ const n = Number(insertAt);
+ if (!Number.isFinite(n)) return null;
+ return Math.floor(n);
+ }, [insertAt]);
+
+ const isInsertAtValid = React.useMemo(() => {
+ return insertAtNumber != null && insertAtNumber >= 1;
+ }, [insertAtNumber]);
+
+ const canConfirmAdd = React.useMemo(() => {
+ return (addSelection as number[]).length > 0 && isInsertAtValid;
+ }, [addSelection, isInsertAtValid]);
+
+ const toggleAddSelectAll = React.useCallback(() => {
+ if (isAllAddSelected) setAddSelection([]);
+ else setAddSelection(addRows.map((r) => r.id));
+ }, [addRows, isAllAddSelected]);
+
+ return (
+
+
+ setFilterCode(e.target.value)}
+ sx={{ width: { xs: "100%", md: 180 } }}
+ helperText=" "
+ placeholder="PP1074 / 1074"
+ />
+
+ setFilterName(e.target.value)}
+ sx={{ width: { xs: "100%", md: 220 } }}
+ helperText=" "
+ />
+
+ setJumpQuery(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") jumpTo();
+ }}
+ sx={{ width: { xs: "100%", md: 260 } }}
+ helperText={t("Enter to jump to item")}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {hasUnsavedChanges && (
+
+ {t("Unsaved changes")}
+
+ )}
+
+
+
+
+
+
+ {t("Order")}
+ {t("Code")}
+ {t("Name")}
+ 出貨倉
+ 入貨倉
+ {t("Actions")}
+
+
+
+ {loading ? (
+
+ {t("Loading")}
+
+ ) : filteredRows.length === 0 ? (
+
+ {t("No data available")}
+
+ ) : (
+ filteredRows.map((r) => (
+ rowRefs.current.set(r.id, el)}
+ hover
+ selected={selectedId === r.id}
+ onClick={() => setSelectionModel([r.id])}
+ sx={{ cursor: "pointer" }}
+ >
+
+
+
+ {r.itemOrder}
+
+
+ {
+ e.stopPropagation();
+ setSelectionModel([r.id]);
+ moveRowByIdTo(r.id, r.itemOrder - 2);
+ }}
+ disabled={r.itemOrder <= 1}
+ sx={{
+ width: 40,
+ height: 30,
+ border: "1px solid",
+ borderColor: "divider",
+ borderRadius: 1,
+ p: 0,
+ }}
+ >
+
+
+ {
+ e.stopPropagation();
+ setSelectionModel([r.id]);
+ moveRowByIdTo(r.id, r.itemOrder);
+ }}
+ disabled={r.itemOrder >= rows.length}
+ sx={{
+ width: 40,
+ height: 30,
+ border: "1px solid",
+ borderColor: "divider",
+ borderRadius: 1,
+ p: 0,
+ mt: 0.5,
+ }}
+ >
+
+
+
+
+
+ {r.code}
+ {r.name}
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/src/components/FinishedGoodManagement/index.ts b/src/components/FinishedGoodManagement/index.ts
new file mode 100644
index 0000000..1039e90
--- /dev/null
+++ b/src/components/FinishedGoodManagement/index.ts
@@ -0,0 +1,4 @@
+import FinishedGoodManagementWrapper from "./FinishedGoodManagementWrapper";
+
+export default FinishedGoodManagementWrapper;
+
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index 2404ee8..bc6f442 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -123,6 +123,12 @@ const NavigationContent: React.FC = () => {
requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN],
path: "/finishedGood",
},
+ {
+ icon: ,
+ label: "Finished Good Management",
+ requiredAbility: [AUTH.ADMIN],
+ path: "/finishedGood/management",
+ },
{
icon: ,
label: "Stock Record",
@@ -396,6 +402,24 @@ const NavigationContent: React.FC = () => {
);
};
+ const selectedLeafPath = React.useMemo(() => {
+ const leafPaths: string[] = [];
+ const walk = (items: NavigationItem[]) => {
+ for (const it of items) {
+ if (it.isHidden) continue;
+ if (!hasAbility(it.requiredAbility)) continue;
+ if (it.path) leafPaths.push(it.path);
+ if (it.children?.length) walk(it.children);
+ }
+ };
+ walk(navigationItems);
+
+ // Pick the most specific (longest) match to avoid double-highlighting
+ const matches = leafPaths.filter((p) => pathname === p || pathname.startsWith(p + "/"));
+ matches.sort((a, b) => b.length - a.length);
+ return matches[0] ?? "";
+ }, [hasAbility, navigationItems, pathname]);
+
const renderNavigationItem = (item: NavigationItem) => {
if (!hasAbility(item.requiredAbility)) {
return null;
@@ -405,10 +429,8 @@ const NavigationContent: React.FC = () => {
const hasVisibleChildren = item.children?.some(child => hasAbility(child.requiredAbility));
const isLeaf = Boolean(item.path);
const isSelected = isLeaf && item.path
- ? pathname === item.path || pathname.startsWith(item.path + "/")
- : hasVisibleChildren && item.children?.some(
- (c) => c.path && (pathname === c.path || pathname.startsWith(c.path + "/"))
- );
+ ? item.path === selectedLeafPath
+ : hasVisibleChildren && item.children?.some((c) => c.path && c.path === selectedLeafPath);
const content = (
{
}}
>
{
primary={t(child.label)}
primaryTypographyProps={{
fontWeight:
- pathname === child.path || pathname.startsWith(`${child.path}/`) ? 600 : 500,
+ child.path === selectedLeafPath ? 600 : 500,
fontSize: "0.875rem",
}}
/>
@@ -509,7 +531,7 @@ const NavigationContent: React.FC = () => {
}}
>
{
primary={t(child.label)}
primaryTypographyProps={{
fontWeight:
- pathname === child.path || pathname.startsWith(`${child.path}/`) ? 600 : 500,
+ child.path === selectedLeafPath ? 600 : 500,
fontSize: "0.875rem",
}}
/>
@@ -538,7 +560,7 @@ const NavigationContent: React.FC = () => {
sx={{ textDecoration: "none", color: "inherit" }}
>
{
primary={t(child.label)}
primaryTypographyProps={{
fontWeight:
- pathname === child.path || (child.path && pathname.startsWith(child.path + "/"))
+ child.path === selectedLeafPath
? 600
: 500,
fontSize: "0.875rem",
diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json
index 7048d7f..825f768 100644
--- a/src/i18n/zh/common.json
+++ b/src/i18n/zh/common.json
@@ -271,6 +271,28 @@
"Management Job Order": "管理工單",
"Search Job Order/ Create Job Order": "搜尋工單/ 建立工單",
"Finished Good Order": "成品出倉",
+ "Finished Good Management": "成品出倉管理",
+ "提料順序": "提料順序",
+ "Filter": "過濾",
+ "Item Code": "材料編號",
+ "Item Name": "材料名稱",
+ "Search & Jump": "搜尋並跳轉",
+ "Enter to jump to item": "按 Enter 直接跳到品項位置",
+ "Jump": "跳轉",
+ "Move Up": "上移",
+ "Move Down": "下移",
+ "Move Top": "置頂",
+ "Move Bottom": "置底",
+ "Add Item": "加入品項",
+ "Refresh": "重新載入",
+ "Unsaved changes": "有未儲存的變更",
+ "Select items without order to append to bottom": "只會顯示尚未設定順序的品項,確認後會加到清單底部",
+ "Only show FG items without order": "請先輸入關鍵字再搜尋(只會查詢未設定順序的品項)",
+ "Insert position must be >= 1": "插入位置必須大於或等於 1",
+ "Insert at": "插入位置",
+ "Order number": "順序號碼",
+ "Order": "順序",
+ "Location": "位置",
"finishedGood": "成品",
"Router": "執貨路線",
"Job Order Pickexcution": "工單提料",
@@ -348,12 +370,10 @@
"BoM Material": "材料清單",
"N/A": "不適用",
"Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度",
- "Item Code": "材料名稱",
"Please scan equipment code": "請掃描設備編號",
"Equipment Code": "設備編號",
"Seq": "步驟",
"SEQ": "步驟",
- "Item Name": "產品名稱",
"Job Order Info": "工單信息",
"Matching Stock": "工單對料",
"No data found": "沒有找到資料",
@@ -439,6 +459,12 @@
"Pass": "通過",
"pass": "通過",
"Actions": "操作",
+ "Insert": "插入",
+ "Move to order": "移動到指定順序",
+ "Target order": "目標順序",
+ "In front of": "前面",
+ "Behind": "後面",
+ "This will move the item to exact order": "確認後會把此品項移到指定順序位置,並自動重排其餘順序",
"View Detail": "查看詳情",
"Back": "返回",
"Back to Truck Lane List": "返回車線列表",