diff --git a/src/app/(main)/m18Syn/layout.tsx b/src/app/(main)/m18Syn/layout.tsx index ecbed38e..32b50acb 100644 --- a/src/app/(main)/m18Syn/layout.tsx +++ b/src/app/(main)/m18Syn/layout.tsx @@ -1,10 +1,24 @@ import { I18nProvider } from "@/i18n"; +import { authOptions } from "@/config/authConfig"; +import { AUTH, hasAbility } from "@/authorities"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; -export default function M18SyncLayout({ +/** FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 */ +export default async function M18SyncLayout({ children, }: { children: React.ReactNode; }) { + const session = await getServerSession(authOptions); + const abilities = session?.user?.abilities ?? []; + const canAccess = + hasAbility(abilities, AUTH.M18_SYNC) || hasAbility(abilities, AUTH.ADMIN); + + if (!canAccess) { + redirect("/dashboard"); + } + return ( {children} diff --git a/src/app/(main)/m18Syn/page.tsx b/src/app/(main)/m18Syn/page.tsx index d935a6d6..df97ff33 100644 --- a/src/app/(main)/m18Syn/page.tsx +++ b/src/app/(main)/m18Syn/page.tsx @@ -26,6 +26,7 @@ function TabPanel(props: TabPanelProps) { ); } +/** FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 */ export default function M18SynPage() { const [tabValue, setTabValue] = useState(0); @@ -270,7 +271,7 @@ export default function M18SynPage() { M18 Sync (by code) - ADMIN only. Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code. + Requires 行政 (ADMIN) or M18同步 (M18_SYNC). Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code. setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth"> diff --git a/src/authorities.ts b/src/authorities.ts index 0ce7f019..ad4aeb9e 100644 --- a/src/authorities.ts +++ b/src/authorities.ts @@ -25,6 +25,8 @@ export const AUTH = { */ PRODUCT_PROCESS: "PRODUCT_PROCESS", REPORT_MGMT: "REPORT_MGMT", + /** FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 — Manual M18 sync page (/m18Syn): ADMIN or this ability */ + M18_SYNC: "M18_SYNC", } as const; /** diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 314e4310..edafb469 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -61,6 +61,7 @@ interface NavigationItem { requiredAbility?: string | string[]; } +/** FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 */ const NavigationContent: React.FC = () => { const { data: session, status } = useSession(); const abilities = session?.user?.abilities ?? []; @@ -241,7 +242,7 @@ const NavigationContent: React.FC = () => { icon: , labelKey: "nav.m18Sync", path: "/m18Syn", - requiredAbility: [AUTH.ADMIN], + requiredAbility: [AUTH.M18_SYNC, AUTH.ADMIN], isHidden: false, }, { diff --git a/src/components/PickOrderSearch/WorkbenchPickExecution.tsx b/src/components/PickOrderSearch/WorkbenchPickExecution.tsx index 98d3442c..03b91061 100644 --- a/src/components/PickOrderSearch/WorkbenchPickExecution.tsx +++ b/src/components/PickOrderSearch/WorkbenchPickExecution.tsx @@ -228,6 +228,16 @@ const isCheckedStatus = (status: string | undefined): boolean => const isRejectedStatus = (status: string | undefined): boolean => String(status || "").toLowerCase() === "rejected"; +const isPendingSolStatus = (status: string | undefined): boolean => { + const s = String(status || "").toLowerCase(); + return ( + s === "pending" || + s === "partially_completed" || + s === "partially_complete" || + s === "" + ); +}; + function safeDisplayTargetDate(targetDate: string | number[]): string { try { if (Array.isArray(targetDate) && targetDate.length >= 3) { @@ -346,7 +356,7 @@ function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] { }); } -/** FP-MTMS Version Checklist | Functions Ref. No. 28 | v1.0.2 | 2026-08-03 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 28 | v1.0.3 | 2026-08-13 */ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -1221,8 +1231,35 @@ const WorkbenchPickExecution: React.FC = ({ filterArgs }) => { releaseProcessedQr(latest); return; } - const expectedPool = activeForUom.length > 0 ? activeForUom : allForUom; - let expectedRow = pickExpectedRowForSubstitution(expectedPool) || allForUom[0]; + // Align DO/JO: when no active suggested lot, bind to pending noLot / unavailable + // rows instead of a completed row that still has lotNo. + let expectedRow: LotRow | undefined; + if (activeForUom.length > 0) { + expectedRow = pickExpectedRowForSubstitution(activeForUom) || allForUom[0]; + } else { + const switchable = allForUom.find( + (r) => + r.stockOutLineId > 0 && + isPendingSolStatus(r.status) && + !isCompletedStatus(r.status) && + !isCheckedStatus(r.status) && + (isNoLotWorkbenchRow(r) || + isRejectedStatus(r.status) || + isInventoryLotLineUnavailable(r) || + isLotAvailabilityExpired(r)), + ); + expectedRow = + switchable || + allForUom.find( + (r) => + r.stockOutLineId > 0 && + isPendingSolStatus(r.status) && + !isCompletedStatus(r.status) && + !isCheckedStatus(r.status), + ) || + pickExpectedRowForSubstitution(allForUom) || + allForUom[0]; + } if (!expectedRow) { setError(t("Scanned item is not found in current line")); startTransition(() => {