完成工單/完成 API 改為僅 ADMIN;「工單生產流程」tab 僅 ADMIN 可見;「已提料」改為全部 POL completed;文案「未提料」→「未完成提料」/Pick incomplete。fix負數倉
| @@ -905,7 +905,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (type?: string | n | |||||
| ); | ); | ||||
| }); | }); | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||||
| export const fetchJoborderProductProcessesPage = cache(async (params: { | export const fetchJoborderProductProcessesPage = cache(async (params: { | ||||
| /** Job order / process date(YYYY-MM-DD) */ | /** Job order / process date(YYYY-MM-DD) */ | ||||
| date?: string | null; | date?: string | null; | ||||
| @@ -20,7 +20,7 @@ export const AUTH = { | |||||
| JOB_MAT: "JOB_MAT", | JOB_MAT: "JOB_MAT", | ||||
| JOB_PROD: "JOB_PROD", | JOB_PROD: "JOB_PROD", | ||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 | |||||
| * 工單 生產流程 完成工單 | * 工單 生產流程 完成工單 | ||||
| */ | */ | ||||
| PRODUCT_PROCESS: "PRODUCT_PROCESS", | PRODUCT_PROCESS: "PRODUCT_PROCESS", | ||||
| @@ -28,7 +28,7 @@ export const AUTH = { | |||||
| } as const; | } as const; | ||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.0 | 2026-08-05 | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 | |||||
| * Match session ability codes (exact, trimmed). | * Match session ability codes (exact, trimmed). | ||||
| */ | */ | ||||
| export function hasAbility( | export function hasAbility( | ||||
| @@ -223,7 +223,7 @@ interface JobOrderOpsTableProps { | |||||
| printerCombo?: PrinterCombo[]; | printerCombo?: PrinterCombo[]; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.4 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.5 | 2026-08-06 */ | |||||
| const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo = [], | printerCombo = [], | ||||
| @@ -232,10 +232,9 @@ const JobOrderOpsTable: React.FC<JobOrderOpsTableProps> = ({ | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const sessionToken = session as SessionWithTokens | null; | const sessionToken = session as SessionWithTokens | null; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | const abilities = session?.abilities ?? session?.user?.abilities ?? []; | ||||
| /** 取消工單:僅 ADMIN */ | |||||
| /** 取消工單 / 完成工單:僅 ADMIN */ | |||||
| const canCancel = hasAbility(abilities, AUTH.ADMIN); | const canCancel = hasAbility(abilities, AUTH.ADMIN); | ||||
| /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單) */ | |||||
| const canComplete = hasAbility(abilities, AUTH.PRODUCT_PROCESS); | |||||
| const canComplete = hasAbility(abilities, AUTH.ADMIN); | |||||
| const labelPrinterCombo = useMemo( | const labelPrinterCombo = useMemo( | ||||
| () => (printerCombo || []).filter((p) => p.type === "Label"), | () => (printerCombo || []).filter((p) => p.type === "Label"), | ||||
| @@ -53,7 +53,7 @@ interface ProductProcessJobOrderDetailProps { | |||||
| initialTabIndex?: number; | initialTabIndex?: number; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||||
| const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| @@ -178,7 +178,7 @@ function isWaitingQcPutAway( | |||||
| return s !== "completed" && s !== "rejected"; | return s !== "completed" && s !== "rejected"; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.5 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ | const ProductProcessList: React.FC<ProductProcessListProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo, | printerCombo, | ||||
| @@ -209,8 +209,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | const abilities = session?.abilities ?? session?.user?.abilities ?? []; | ||||
| /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單);不含 ADMIN 以免群組 ADMIN 繞過勾選 */ | |||||
| const canManageUpdateJo = hasAbility(abilities, AUTH.PRODUCT_PROCESS); | |||||
| /** 完成工單:僅 ADMIN */ | |||||
| const canManageUpdateJo = hasAbility(abilities, AUTH.ADMIN); | |||||
| type ProcessFilter = "all" | "drink" | "Powder_Mixture" | "other"; | type ProcessFilter = "all" | "drink" | "Powder_Mixture" | "other"; | ||||
| const listTab = normalizeListTab(listPersistedState.pickBucket); | const listTab = normalizeListTab(listPersistedState.pickBucket); | ||||
| @@ -1230,20 +1230,17 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| {t("Matching Stock")} | {t("Matching Stock")} | ||||
| </Button> | </Button> | ||||
| {statusLower !== "completed" && ( | |||||
| {canManageUpdateJo && statusLower !== "completed" && ( | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| disabled={!canManageUpdateJo} | |||||
| onClick={() => | onClick={() => | ||||
| canManageUpdateJo | |||||
| ? openConfirm( | |||||
| t("Confirm to update this Job Order?"), | |||||
| async () => { | |||||
| await handleUpdateJo(process); | |||||
| }, | |||||
| ) | |||||
| : undefined | |||||
| openConfirm( | |||||
| t("Confirm to update this Job Order?"), | |||||
| async () => { | |||||
| await handleUpdateJo(process); | |||||
| }, | |||||
| ) | |||||
| } | } | ||||
| > | > | ||||
| {t("Update Job Order")} | {t("Update Job Order")} | ||||
| @@ -17,6 +17,7 @@ import DrinkProductionQtyDashboard from "@/components/ProductionProcess/DrinkPro | |||||
| import JobOrderOpsTable from "@/components/ProductionProcess/JobOrderOpsTable"; | import JobOrderOpsTable from "@/components/ProductionProcess/JobOrderOpsTable"; | ||||
| import type { PrinterCombo } from "@/app/api/settings/printer"; | import type { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { AUTH, hasAbility } from "@/authorities"; | |||||
| interface ProductionProcessPageProps { | interface ProductionProcessPageProps { | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| @@ -24,9 +25,17 @@ interface ProductionProcessPageProps { | |||||
| const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | ||||
| type PageTab = | |||||
| | "list" | |||||
| | "ops" | |||||
| | "jobProcessStatus" | |||||
| | "operatorKpi" | |||||
| | "equipmentStatus" | |||||
| | "drinkQty"; | |||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20 | * FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20 | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.4 | 2026-08-06 | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 47 | v1.0.5 | 2026-08-06 | |||||
| */ | */ | ||||
| const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | ||||
| const { t } = useTranslation(["common", "productionProcess"]); | const { t } = useTranslation(["common", "productionProcess"]); | ||||
| @@ -36,19 +45,27 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| productProcessId: number; | productProcessId: number; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| } | null>(null); | } | null>(null); | ||||
| /** 0 = Production Process list; 1 = JO ops table; 2..5 = dashboards */ | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [pageTab, setPageTab] = useState<PageTab>("list"); | |||||
| const [productionListState, setProductionListState] = useState(() => ({ | const [productionListState, setProductionListState] = useState(() => ({ | ||||
| ...createDefaultProductionProcessListPersistedState(), | ...createDefaultProductionProcessListPersistedState(), | ||||
| })); | })); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const sessionToken = session as SessionWithTokens | null; | const sessionToken = session as SessionWithTokens | null; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | |||||
| /** 「查看工單流程情況」僅 ADMIN */ | |||||
| const canSeeOpsTable = hasAbility(abilities, AUTH.ADMIN); | |||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| const pathname = usePathname(); | const pathname = usePathname(); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [linkQcOpen, setLinkQcOpen] = useState(false); | const [linkQcOpen, setLinkQcOpen] = useState(false); | ||||
| const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null); | const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null); | ||||
| useEffect(() => { | |||||
| if (!canSeeOpsTable && pageTab === "ops") { | |||||
| setPageTab("list"); | |||||
| } | |||||
| }, [canSeeOpsTable, pageTab]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
| try { | try { | ||||
| @@ -83,8 +100,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => { | |||||
| setTabIndex(newValue); | |||||
| const handleTabChange = useCallback((_: React.SyntheticEvent, newValue: PageTab) => { | |||||
| setPageTab(newValue); | |||||
| }, []); | }, []); | ||||
| const openStockInLineIdQ = searchParams.get("openStockInLineId"); | const openStockInLineIdQ = searchParams.get("openStockInLineId"); | ||||
| @@ -100,7 +117,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| if (!Number.isFinite(id) || id <= 0) return; | if (!Number.isFinite(id) || id <= 0) return; | ||||
| setSelectedProcessId(null); | setSelectedProcessId(null); | ||||
| setSelectedMatchingStock(null); | setSelectedMatchingStock(null); | ||||
| setTabIndex(0); | |||||
| setPageTab("list"); | |||||
| setProductionListState((prev) => ({ | setProductionListState((prev) => ({ | ||||
| ...prev, | ...prev, | ||||
| pickBucket: "pending_qc", | pickBucket: "pending_qc", | ||||
| @@ -144,16 +161,18 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Box> | <Box> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | |||||
| <Tab label={t("Production Process")} /> | |||||
| <Tab label={t("Job Order Ops Table")} /> | |||||
| <Tab label={t("Job Process Status Dashboard")} /> | |||||
| <Tab label={t("Operator KPI Dashboard")} /> | |||||
| <Tab label={t("Production Equipment Status Dashboard")} /> | |||||
| <Tab label={t("Drink Production Qty Dashboard")} /> | |||||
| <Tabs value={pageTab} onChange={handleTabChange} sx={{ mb: 2 }}> | |||||
| <Tab value="list" label={t("Production Process")} /> | |||||
| {canSeeOpsTable && ( | |||||
| <Tab value="ops" label={t("Job Order Ops Table")} /> | |||||
| )} | |||||
| <Tab value="jobProcessStatus" label={t("Job Process Status Dashboard")} /> | |||||
| <Tab value="operatorKpi" label={t("Operator KPI Dashboard")} /> | |||||
| <Tab value="equipmentStatus" label={t("Production Equipment Status Dashboard")} /> | |||||
| <Tab value="drinkQty" label={t("Drink Production Qty Dashboard")} /> | |||||
| </Tabs> | </Tabs> | ||||
| {tabIndex === 0 && ( | |||||
| {pageTab === "list" && ( | |||||
| <ProductionProcessList | <ProductionProcessList | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| listPersistedState={productionListState} | listPersistedState={productionListState} | ||||
| @@ -174,7 +193,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 1 && ( | |||||
| {pageTab === "ops" && canSeeOpsTable && ( | |||||
| <JobOrderOpsTable | <JobOrderOpsTable | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| onSelectProcess={(jobOrderId) => { | onSelectProcess={(jobOrderId) => { | ||||
| @@ -183,16 +202,16 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 2 && ( | |||||
| {pageTab === "jobProcessStatus" && ( | |||||
| <JobProcessStatus /> | <JobProcessStatus /> | ||||
| )} | )} | ||||
| {tabIndex === 3 && ( | |||||
| {pageTab === "operatorKpi" && ( | |||||
| <OperatorKpiDashboard /> | <OperatorKpiDashboard /> | ||||
| )} | )} | ||||
| {tabIndex === 4 && ( | |||||
| {pageTab === "equipmentStatus" && ( | |||||
| <EquipmentStatusDashboard /> | <EquipmentStatusDashboard /> | ||||
| )} | )} | ||||
| {tabIndex === 5 && ( | |||||
| {pageTab === "drinkQty" && ( | |||||
| <DrinkProductionQtyDashboard /> | <DrinkProductionQtyDashboard /> | ||||
| )} | )} | ||||
| </Box> | </Box> | ||||
| @@ -79,11 +79,11 @@ | |||||
| "Issue": "Issue", | "Issue": "Issue", | ||||
| "Job Order Ops Table": "Job Order Ops Table", | "Job Order Ops Table": "Job Order Ops Table", | ||||
| "Pending (picked)": "Picked", | "Pending (picked)": "Picked", | ||||
| "Pending (not picked)": "Not picked", | |||||
| "Pending (not picked)": "Pick incomplete", | |||||
| "Processing (picked)": "Picked", | "Processing (picked)": "Picked", | ||||
| "Processing (not picked)": "Not picked", | |||||
| "Processing (not picked)": "Pick incomplete", | |||||
| "Picked": "Picked", | "Picked": "Picked", | ||||
| "Not picked": "Not picked", | |||||
| "Not picked": "Pick incomplete", | |||||
| "Stop (paused)": "Stop (paused)", | "Stop (paused)": "Stop (paused)", | ||||
| "Cancelled": "Cancelled", | "Cancelled": "Cancelled", | ||||
| "Reload data": "Reload data", | "Reload data": "Reload data", | ||||
| @@ -224,10 +224,10 @@ | |||||
| "Off-plan unfinished": "Off-plan unfinished", | "Off-plan unfinished": "Off-plan unfinished", | ||||
| "All unfinished": "Needs action", | "All unfinished": "Needs action", | ||||
| "Needs action": "Needs action", | "Needs action": "Needs action", | ||||
| "Not picked · Not started": "Not picked · Not started", | |||||
| "Not picked · Not started": "Pick incomplete · Not started", | |||||
| "Picked · Not started": "Picked · Not started", | "Picked · Not started": "Picked · Not started", | ||||
| "Picked · In production": "Picked · In production", | "Picked · In production": "Picked · In production", | ||||
| "Not picked · In production": "Not picked · In production", | |||||
| "Not picked · In production": "Pick incomplete · In production", | |||||
| "Powder_Mixture": "Powder Mixture", | "Powder_Mixture": "Powder Mixture", | ||||
| "Processing": "Processing", | "Processing": "Processing", | ||||
| "Total lines: ": "Total lines: ", | "Total lines: ": "Total lines: ", | ||||
| @@ -79,11 +79,11 @@ | |||||
| "Issue": "異常", | "Issue": "異常", | ||||
| "Job Order Ops Table": "工單生產流程", | "Job Order Ops Table": "工單生產流程", | ||||
| "Pending (picked)": "已提料", | "Pending (picked)": "已提料", | ||||
| "Pending (not picked)": "未提料", | |||||
| "Pending (not picked)": "未完成提料", | |||||
| "Processing (picked)": "已提料", | "Processing (picked)": "已提料", | ||||
| "Processing (not picked)": "未提料", | |||||
| "Processing (not picked)": "未完成提料", | |||||
| "Picked": "已提料", | "Picked": "已提料", | ||||
| "Not picked": "未提料", | |||||
| "Not picked": "未完成提料", | |||||
| "Stop (paused)": "暫停中", | "Stop (paused)": "暫停中", | ||||
| "Cancelled": "已取消", | "Cancelled": "已取消", | ||||
| "Reload data": "重新載入", | "Reload data": "重新載入", | ||||
| @@ -224,10 +224,10 @@ | |||||
| "Off-plan unfinished": "未按規劃完成工單", | "Off-plan unfinished": "未按規劃完成工單", | ||||
| "All unfinished": "需處理", | "All unfinished": "需處理", | ||||
| "Needs action": "需處理", | "Needs action": "需處理", | ||||
| "Not picked · Not started": "未提料 · 未開工", | |||||
| "Not picked · Not started": "未完成提料 · 未開工", | |||||
| "Picked · Not started": "已提料 · 未開工", | "Picked · Not started": "已提料 · 未開工", | ||||
| "Picked · In production": "已提料 · 未完成生產", | "Picked · In production": "已提料 · 未完成生產", | ||||
| "Not picked · In production": "未提料 · 未完成生產", | |||||
| "Not picked · In production": "未完成提料 · 未完成生產", | |||||
| "Powder_Mixture": "箱料粉", | "Powder_Mixture": "箱料粉", | ||||
| "Processing": "生產中", | "Processing": "生產中", | ||||
| "Total lines: ": "總數量:", | "Total lines: ": "總數量:", | ||||