| @@ -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 ( | |||
| <I18nProvider namespaces={["m18Sync", "navigation", "common"]}> | |||
| {children} | |||
| @@ -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) | |||
| </Typography> | |||
| <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}> | |||
| 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. | |||
| </Typography> | |||
| <Tabs value={tabValue} onChange={(_, v) => setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth"> | |||
| @@ -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; | |||
| /** | |||
| @@ -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: <Sync />, | |||
| labelKey: "nav.m18Sync", | |||
| path: "/m18Syn", | |||
| requiredAbility: [AUTH.ADMIN], | |||
| requiredAbility: [AUTH.M18_SYNC, AUTH.ADMIN], | |||
| isHidden: false, | |||
| }, | |||
| { | |||
| @@ -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<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -1221,8 +1231,35 @@ const WorkbenchPickExecution: React.FC<Props> = ({ 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(() => { | |||