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/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 04d6d8a..ad0aba7 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -54,6 +54,8 @@ const pathToLabelMap: { [path: string]: string } = { "/bagPrint": "打袋機", "/laserPrint": "檸檬機(激光機)", "/settings/itemPrice": "Price Inquiry", + "/finishedGood": "Finished Good Order", + "/finishedGood/management": "Finished Good Management", }; const Breadcrumb = () => { 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} + + + + + + + )) + )} + +
+
+ +
+ + setAddOpen(false)} fullWidth maxWidth="md"> + {t("Add Item")} + + + setAddSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") searchAdd(); + }} + sx={{ width: { xs: "100%", md: 420 } }} + helperText=" " + /> + { + const v = e.target.value; + if (v === "") return setInsertAt(""); + const n = Number(v); + if (!Number.isFinite(n)) return; + // prevent 0/negative; still allow user to type then show error + setInsertAt(n); + }} + inputProps={{ min: 1 }} + sx={{ width: { xs: "100%", md: 160 } }} + error={insertAt !== "" && !isInsertAtValid} + helperText={ + insertAt !== "" && !isInsertAtValid ? t("Insert position must be >= 1") : t("Order number") + } + /> + + setInsertPlacement(e.target.value as "before" | "after")} + > + } label={t("In front of")} /> + } label={t("Behind")} /> + + + + + {t("Only show FG items without order")} + + + + + + + + + + + + {t("Code")} + {t("Name")} + 出貨倉 + 入貨倉 + + + + {addLoading ? ( + + {t("Loading")} + + ) : addRows.length === 0 ? ( + + {t("No data available")} + + ) : ( + addRows.map((r) => { + const checked = (addSelection as number[]).includes(r.id); + return ( + + + { + setAddSelection((prev) => { + const set = new Set(prev as number[]); + if (set.has(r.id)) set.delete(r.id); + else set.add(r.id); + return Array.from(set); + }); + }} + /> + + {r.code} + {r.name} + + + + ); + }) + )} + +
+
+ +
+
+ + + + +
+ + setMoveDialogOpen(false)} maxWidth="xs" fullWidth> + {t("Move to order")} + + + { + const v = e.target.value; + if (v === "") return setMoveToOrder(""); + const n = Number(v); + if (!Number.isFinite(n)) return; + setMoveToOrder(n); + }} + inputProps={{ min: 1 }} + onKeyDown={(e) => { + if (e.key === "Enter") confirmMoveDialog(); + }} + /> + setMovePlacement(e.target.value as "before" | "after")} + > + } label={t("In front of")} /> + } label={t("Behind")} /> + + + {t("This will move the item to exact order")} + + + + + + + + +
+ ); +} + 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 86e9bb2..7e367ca 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", @@ -404,6 +410,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; @@ -413,10 +437,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", }} /> @@ -517,7 +539,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", }} /> @@ -546,7 +568,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": "返回車線列表",