| @@ -906,7 +906,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (type?: string | n | |||||
| ); | ); | ||||
| }); | }); | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||||
| 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; | ||||
| @@ -924,4 +924,78 @@ export const importTruckLaneScheduleExcelAction = async ( | |||||
| ); | ); | ||||
| } | } | ||||
| return (await response.json()) as TruckLaneScheduleResponse[]; | return (await response.json()) as TruckLaneScheduleResponse[]; | ||||
| }; | |||||
| export interface PreviewTruckLaneRenameRequest { | |||||
| truckLanceCode: string; | |||||
| storeId: string; | |||||
| remark?: string | null; | |||||
| } | |||||
| export interface PreviewTruckLaneRenameResponse { | |||||
| unfinishedCount: number; | |||||
| completedCount: number; | |||||
| handlerNames: string[]; | |||||
| openScheduleCount: number; | |||||
| } | |||||
| export interface RenameTruckLaneRequest { | |||||
| truckLanceCode: string; | |||||
| storeId: string; | |||||
| remark?: string | null; | |||||
| newTruckLanceCode: string; | |||||
| departureTimeChanged: boolean; | |||||
| newDepartureTime?: string | null; | |||||
| } | |||||
| export interface RenameTruckLaneResponse { | |||||
| truckLanceCode: string; | |||||
| ticketCount: number; | |||||
| scheduleLineCount: number; | |||||
| } | |||||
| export interface TruckLaneRenameLogResponse { | |||||
| id: number; | |||||
| created?: string | null; | |||||
| createdBy?: string | null; | |||||
| oldCode: string; | |||||
| newCode: string; | |||||
| storeId: string; | |||||
| remark?: string | null; | |||||
| oldDepartureTime?: string | null; | |||||
| newDepartureTime?: string | null; | |||||
| ticketCount: number; | |||||
| /** `truck.id` of shops in the renamed lane, comma-separated. */ | |||||
| truckIds?: string | null; | |||||
| } | |||||
| export const previewTruckLaneRenameAction = async ( | |||||
| data: PreviewTruckLaneRenameRequest, | |||||
| ) => { | |||||
| return serverFetchJson<PreviewTruckLaneRenameResponse>( | |||||
| `${BASE_API_URL}/truck/previewTruckLaneRename`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const renameTruckLaneAction = async (data: RenameTruckLaneRequest) => { | |||||
| return serverFetchJson<RenameTruckLaneResponse>( | |||||
| `${BASE_API_URL}/truck/renameTruckLane`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const listTruckLaneRenameLogsAction = async () => { | |||||
| return serverFetchJson<TruckLaneRenameLogResponse[]>( | |||||
| `${BASE_API_URL}/truck/truckLaneRenameLog`, | |||||
| { method: "GET", headers: { "Content-Type": "application/json" } }, | |||||
| ); | |||||
| }; | }; | ||||
| @@ -66,7 +66,12 @@ import { | |||||
| type SaveTruckRequest, | type SaveTruckRequest, | ||||
| type UpdateTruckShopDetailsRequest, | type UpdateTruckShopDetailsRequest, | ||||
| type CreateTruckWithoutShopRequest, | type CreateTruckWithoutShopRequest, | ||||
| type MessageResponse | |||||
| type MessageResponse, | |||||
| previewTruckLaneRenameAction, | |||||
| renameTruckLaneAction, | |||||
| listTruckLaneRenameLogsAction, | |||||
| type PreviewTruckLaneRenameRequest, | |||||
| type RenameTruckLaneRequest, | |||||
| } from "./actions"; | } from "./actions"; | ||||
| export const fetchAllShopsClient = async (params?: Record<string, string | number | null>) => { | export const fetchAllShopsClient = async (params?: Record<string, string | number | null>) => { | ||||
| @@ -276,6 +281,20 @@ export const parseTruckLaneScheduleExcelClient = async ( | |||||
| return await parseTruckLaneScheduleExcelAction(formData, defaultExecuteAt); | return await parseTruckLaneScheduleExcelAction(formData, defaultExecuteAt); | ||||
| }; | }; | ||||
| export const previewTruckLaneRenameClient = async ( | |||||
| data: PreviewTruckLaneRenameRequest, | |||||
| ) => { | |||||
| return await previewTruckLaneRenameAction(data); | |||||
| }; | |||||
| export const renameTruckLaneClient = async (data: RenameTruckLaneRequest) => { | |||||
| return await renameTruckLaneAction(data); | |||||
| }; | |||||
| export const listTruckLaneRenameLogsClient = async () => { | |||||
| return await listTruckLaneRenameLogsAction(); | |||||
| }; | |||||
| export const importTruckLaneScheduleExcelClient = async ( | export const importTruckLaneScheduleExcelClient = async ( | ||||
| formData: FormData, | formData: FormData, | ||||
| defaultExecuteAt?: string | null, | defaultExecuteAt?: string | null, | ||||
| @@ -203,13 +203,13 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| return '-'; | return '-'; | ||||
| }; | }; | ||||
| // Compare a timestamp against the selected date (today/tomorrow/...) and return a short label. | |||||
| // Used to avoid confusion when picking crosses midnight. | |||||
| // Compare a timestamp against calendar today. Same-day picks show no chip; | |||||
| // only midnight-crossing (yesterday / tomorrow / N days) gets a label. | |||||
| // Does not filter rows — trucklane tickets for the selected delivery date still show. | |||||
| const relativeDayLabel = useCallback( | const relativeDayLabel = useCallback( | ||||
| (dateTimeData: string | number[] | null, dateOption: string): string | null => { | |||||
| (dateTimeData: string | number[] | null): string | null => { | |||||
| if (!dateTimeData) return null; | if (!dateTimeData) return null; | ||||
| const dateOffset = getDateOffset(dateOption); | |||||
| const baseDate = dayjs().startOf('day').add(dateOffset, 'day'); | |||||
| const baseDate = dayjs().startOf('day'); | |||||
| let dt: dayjs.Dayjs; | let dt: dayjs.Dayjs; | ||||
| if (Array.isArray(dateTimeData)) { | if (Array.isArray(dateTimeData)) { | ||||
| @@ -518,8 +518,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||||
| filteredData.map((row, index) => { | filteredData.map((row, index) => { | ||||
| const timeRemaining = calculateTimeRemaining(row, selectedDate); | const timeRemaining = calculateTimeRemaining(row, selectedDate); | ||||
| const chipColor = getTimeChipColor(row, selectedDate); | const chipColor = getTimeChipColor(row, selectedDate); | ||||
| const startRel = relativeDayLabel(row.firstTicketStartTime, selectedDate); | |||||
| const endRel = relativeDayLabel(row.lastTicketEndTime, selectedDate); | |||||
| const startRel = relativeDayLabel(row.firstTicketStartTime); | |||||
| const endRel = relativeDayLabel(row.lastTicketEndTime); | |||||
| return ( | return ( | ||||
| <TableRow | <TableRow | ||||
| @@ -91,6 +91,12 @@ export interface WorkbenchLotLabelPrintModalProps { | |||||
| /** 與 statusTitleText 搭配;預設 error(舊版固定紅字) */ | /** 與 statusTitleText 搭配;預設 error(舊版固定紅字) */ | ||||
| statusTitleSeverity?: "success" | "warning" | "error"; | statusTitleSeverity?: "success" | "warning" | "error"; | ||||
| warehouseCodePrefixFilter?: string; | warehouseCodePrefixFilter?: string; | ||||
| /** | |||||
| * Hide lots whose warehouse code equals an entry or starts with `{entry}-`. | |||||
| * JO passes `4F` so the pick-station list matches the suggestion block list. | |||||
| * The row that opened the dialog stays visible. | |||||
| */ | |||||
| excludeWarehouseCodePrefixes?: string[]; | |||||
| /** | /** | ||||
| * When true, omit the API 「scanned」 lot from the merged list (legacy FG-style). | * When true, omit the API 「scanned」 lot from the merged list (legacy FG-style). | ||||
| * Workbench should leave false so the current row’s lot appears for label print / scan-pick. | * Workbench should leave false so the current row’s lot appears for label print / scan-pick. | ||||
| @@ -147,7 +153,7 @@ function isLabelPrinter(p: Printer): boolean { | |||||
| return s.includes("label") && !s.includes("a4"); | return s.includes("label") && !s.includes("a4"); | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.1 | 2026-07-22 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.2 | 2026-11-02 */ | |||||
| const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ | const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -159,6 +165,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| statusTitleText, | statusTitleText, | ||||
| statusTitleSeverity = "error", | statusTitleSeverity = "error", | ||||
| warehouseCodePrefixFilter, | warehouseCodePrefixFilter, | ||||
| excludeWarehouseCodePrefixes, | |||||
| hideTriggeredLot = false, | hideTriggeredLot = false, | ||||
| triggerLotAvailableQty = null, | triggerLotAvailableQty = null, | ||||
| triggerLotUom = null, | triggerLotUom = null, | ||||
| @@ -450,14 +457,22 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| ]); | ]); | ||||
| const filteredLots = useMemo(() => { | const filteredLots = useMemo(() => { | ||||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | |||||
| if (!prefix) return availableLots; | |||||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim().toUpperCase(); | |||||
| const blocked = (excludeWarehouseCodePrefixes ?? []) | |||||
| .map((entry) => entry.trim().toUpperCase()) | |||||
| .filter((entry) => entry.length > 0); | |||||
| return availableLots.filter((lot) => { | return availableLots.filter((lot) => { | ||||
| // 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號 | |||||
| // 使用者從本列開啟視窗:即使倉位在排除清單,仍顯示目前這筆批號 | |||||
| if (lot._scanned) return true; | if (lot._scanned) return true; | ||||
| return String(lot.warehouseCode ?? "").startsWith(prefix); | |||||
| const code = String(lot.warehouseCode ?? "").trim().toUpperCase(); | |||||
| const blockedHit = blocked.some( | |||||
| (entry) => code === entry || code.startsWith(`${entry}-`), | |||||
| ); | |||||
| if (blockedHit) return false; | |||||
| if (!prefix) return true; | |||||
| return code.startsWith(prefix); | |||||
| }); | }); | ||||
| }, [availableLots, warehouseCodePrefixFilter]); | |||||
| }, [availableLots, warehouseCodePrefixFilter, excludeWarehouseCodePrefixes]); | |||||
| const selectedPrinter = useMemo(() => { | const selectedPrinter = useMemo(() => { | ||||
| if (selectedPrinterId === "") return null; | if (selectedPrinterId === "") return null; | ||||
| @@ -618,7 +618,7 @@ const QrCodeModal: React.FC<{ | |||||
| ); | ); | ||||
| }; | }; | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.1 | 2026-08-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.2 | 2026-11-02 */ | |||||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | ||||
| const workbenchMode = true; | const workbenchMode = true; | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| @@ -4642,6 +4642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| workbenchLotLabelContextLot != null | workbenchLotLabelContextLot != null | ||||
| } | } | ||||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | warehouseCodePrefixFilter={lotFloorPrefixFilter} | ||||
| excludeWarehouseCodePrefixes={["4F"]} | |||||
| triggerLotAvailableQty={ | triggerLotAvailableQty={ | ||||
| workbenchLotLabelContextLot != null | workbenchLotLabelContextLot != null | ||||
| ? Number(workbenchLotLabelContextLot.availableQty) | ? Number(workbenchLotLabelContextLot.availableQty) | ||||
| @@ -251,7 +251,7 @@ const NavigationContent: React.FC = () => { | |||||
| icon: <ShowChart />, | icon: <ShowChart />, | ||||
| labelKey: "nav.chartReports", | labelKey: "nav.chartReports", | ||||
| path: "", | path: "", | ||||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN], | |||||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN, AUTH.PERFORMANCE], | |||||
| isHidden: false, | isHidden: false, | ||||
| children: [ | children: [ | ||||
| { | { | ||||
| @@ -280,7 +280,7 @@ const NavigationContent: React.FC = () => { | |||||
| icon: <LocalShipping />, | icon: <LocalShipping />, | ||||
| labelKey: "nav.chart.delivery", | labelKey: "nav.chart.delivery", | ||||
| path: "/chart/delivery", | path: "/chart/delivery", | ||||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN], | |||||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN, AUTH.PERFORMANCE], | |||||
| }, | }, | ||||
| { | { | ||||
| id: "nav.chart.warehouse", | id: "nav.chart.warehouse", | ||||
| @@ -17,25 +17,42 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { UpdateProductProcessLineQtyRequest } from "@/app/api/jo/actions"; | import { UpdateProductProcessLineQtyRequest } from "@/app/api/jo/actions"; | ||||
| type QtyInput = number | ""; | |||||
| export type ProductionOutputFormData = Omit< | |||||
| UpdateProductProcessLineQtyRequest, | |||||
| | "outputFromProcessQty" | |||||
| | "byproductQty" | |||||
| | "defectQty" | |||||
| | "defect2Qty" | |||||
| | "defect3Qty" | |||||
| | "scrapQty" | |||||
| > & { | |||||
| outputFromProcessQty: QtyInput; | |||||
| byproductQty: QtyInput; | |||||
| defectQty: QtyInput; | |||||
| defect2Qty: QtyInput; | |||||
| defect3Qty: QtyInput; | |||||
| scrapQty: QtyInput; | |||||
| }; | |||||
| const parseQtyInput = (raw: string): QtyInput => { | |||||
| if (raw === "") return ""; | |||||
| const n = parseInt(raw, 10); | |||||
| return Number.isNaN(n) ? "" : n; | |||||
| }; | |||||
| export const toSubmitQty = (v: QtyInput | null | undefined): number => | |||||
| v === "" || v == null || Number.isNaN(Number(v)) ? 0 : Number(v); | |||||
| interface ProductionOutputFormProps { | interface ProductionOutputFormProps { | ||||
| outputData: (UpdateProductProcessLineQtyRequest & { | |||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| }); | |||||
| setOutputData: React.Dispatch< | |||||
| React.SetStateAction< | |||||
| UpdateProductProcessLineQtyRequest & { | |||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| } | |||||
| > | |||||
| >; | |||||
| outputData: ProductionOutputFormData; | |||||
| setOutputData: React.Dispatch<React.SetStateAction<ProductionOutputFormData>>; | |||||
| onSubmit: () => Promise<void> | void; | onSubmit: () => Promise<void> | void; | ||||
| onCancel: () => void; | onCancel: () => void; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||||
| const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | ||||
| outputData, | outputData, | ||||
| setOutputData, | setOutputData, | ||||
| @@ -68,7 +85,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||||
| onChange={(e) => | onChange={(e) => | ||||
| setOutputData({ | setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| outputFromProcessQty: parseInt(e.target.value) || 0, | |||||
| outputFromProcessQty: parseQtyInput(e.target.value), | |||||
| }) | }) | ||||
| } | } | ||||
| /> | /> | ||||
| @@ -103,7 +120,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||||
| onChange={(e) => | onChange={(e) => | ||||
| setOutputData({ | setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| byproductQty: parseInt(e.target.value) || 0, | |||||
| byproductQty: parseQtyInput(e.target.value), | |||||
| }) | }) | ||||
| } | } | ||||
| /> | /> | ||||
| @@ -138,7 +155,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||||
| onChange={(e) => | onChange={(e) => | ||||
| setOutputData({ | setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defectQty: parseInt(e.target.value) || 0, | |||||
| defectQty: parseQtyInput(e.target.value), | |||||
| }) | }) | ||||
| } | } | ||||
| /> | /> | ||||
| @@ -173,7 +190,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||||
| onChange={(e) => | onChange={(e) => | ||||
| setOutputData({ | setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| scrapQty: parseInt(e.target.value) || 0, | |||||
| scrapQty: parseQtyInput(e.target.value), | |||||
| }) | }) | ||||
| } | } | ||||
| /> | /> | ||||
| @@ -207,4 +224,4 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default ProductionOutputForm; | |||||
| export default ProductionOutputForm; | |||||
| @@ -5,47 +5,47 @@ import { | |||||
| Button, | Button, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| fetchProductProcessLineDetail, | fetchProductProcessLineDetail, | ||||
| updateProductProcessLineQty, | updateProductProcessLineQty, | ||||
| UpdateProductProcessLineQtyRequest, | |||||
| JobOrderProcessLineDetailResponse, | JobOrderProcessLineDetailResponse, | ||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import ProductionOutputForm from "./ProductionOutputForm"; | |||||
| import ProductionOutputForm, { | |||||
| ProductionOutputFormData, | |||||
| toSubmitQty, | |||||
| } from "./ProductionOutputForm"; | |||||
| interface ProductionOutputFormPageProps { | interface ProductionOutputFormPageProps { | ||||
| lineId: number | null; | lineId: number | null; | ||||
| onBack: () => void; | onBack: () => void; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||||
| const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | ||||
| lineId, | lineId, | ||||
| onBack, | onBack, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | |||||
| const { t } = useTranslation(["common", "jo", "productionProcess"]); | |||||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | ||||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| }>({ | |||||
| const [outputData, setOutputData] = useState<ProductionOutputFormData>({ | |||||
| productProcessLineId: lineId ?? 0, | productProcessLineId: lineId ?? 0, | ||||
| outputFromProcessQty: 0, | |||||
| outputFromProcessQty: "", | |||||
| outputFromProcessUom: "", | outputFromProcessUom: "", | ||||
| defectQty: 0, | |||||
| defectQty: "", | |||||
| defectUom: "", | defectUom: "", | ||||
| defect2Qty: 0, | |||||
| defect2Qty: "", | |||||
| defect2Uom: "", | defect2Uom: "", | ||||
| defect3Qty: 0, | |||||
| defect3Qty: "", | |||||
| defect3Uom: "", | defect3Uom: "", | ||||
| defectDescription: "", | defectDescription: "", | ||||
| defectDescription2: "", | defectDescription2: "", | ||||
| defectDescription3: "", | defectDescription3: "", | ||||
| scrapQty: 0, | |||||
| scrapQty: "", | |||||
| scrapUom: "", | scrapUom: "", | ||||
| byproductName: "", | byproductName: "", | ||||
| byproductQty: 0, | |||||
| byproductQty: "", | |||||
| byproductUom: "", | byproductUom: "", | ||||
| }); | }); | ||||
| @@ -60,14 +60,14 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| setOutputData((prev) => ({ | setOutputData((prev) => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | productProcessLineId: detail.id, | ||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | outputFromProcessUom: (detail as any).outputFromProcessUom || "", | ||||
| defectQty: detail.defectQty || 0, | |||||
| defectQty: detail.defectQty || "", | |||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| scrapQty: detail.scrapQty || 0, | |||||
| scrapQty: detail.scrapQty || "", | |||||
| scrapUom: detail.scrapUom || "", | scrapUom: detail.scrapUom || "", | ||||
| byproductName: detail.byproductName || "", | byproductName: detail.byproductName || "", | ||||
| byproductQty: detail.byproductQty || 0, | |||||
| byproductQty: detail.byproductQty || "", | |||||
| byproductUom: detail.byproductUom || "", | byproductUom: detail.byproductUom || "", | ||||
| })); | })); | ||||
| }) | }) | ||||
| @@ -84,6 +84,12 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| await updateProductProcessLineQty({ | await updateProductProcessLineQty({ | ||||
| ...outputData, | ...outputData, | ||||
| productProcessLineId: lineDetail.id || 0, | productProcessLineId: lineDetail.id || 0, | ||||
| outputFromProcessQty: toSubmitQty(outputData.outputFromProcessQty), | |||||
| byproductQty: toSubmitQty(outputData.byproductQty), | |||||
| defectQty: toSubmitQty(outputData.defectQty), | |||||
| defect2Qty: toSubmitQty(outputData.defect2Qty), | |||||
| defect3Qty: toSubmitQty(outputData.defect3Qty), | |||||
| scrapQty: toSubmitQty(outputData.scrapQty), | |||||
| }); | }); | ||||
| console.log("Output data submitted successfully"); | console.log("Output data submitted successfully"); | ||||
| @@ -94,14 +100,14 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| setOutputData((prev) => ({ | setOutputData((prev) => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | productProcessLineId: detail.id, | ||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | outputFromProcessUom: (detail as any).outputFromProcessUom || "", | ||||
| defectQty: detail.defectQty || 0, | |||||
| defectQty: detail.defectQty || "", | |||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| scrapQty: detail.scrapQty || 0, | |||||
| scrapQty: detail.scrapQty || "", | |||||
| scrapUom: detail.scrapUom || "", | scrapUom: detail.scrapUom || "", | ||||
| byproductName: detail.byproductName || "", | byproductName: detail.byproductName || "", | ||||
| byproductQty: detail.byproductQty || 0, | |||||
| byproductQty: detail.byproductQty || "", | |||||
| byproductUom: detail.byproductUom || "", | byproductUom: detail.byproductUom || "", | ||||
| })); | })); | ||||
| @@ -116,8 +122,13 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <Button variant="outlined" onClick={onBack}> | |||||
| {t("Back to List")} | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={onBack} | |||||
| startIcon={<ArrowBackIcon />} | |||||
| > | |||||
| {t("Back to Process Steps")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -64,13 +64,16 @@ interface ProductProcessDetailProps { | |||||
| jobOrderId: number; | jobOrderId: number; | ||||
| onBack: () => void; | onBack: () => void; | ||||
| fromJosave?: boolean; | fromJosave?: boolean; | ||||
| /** 進入步驟執行頁時通知父層(用來隱藏「返回列表」) */ | |||||
| onStepViewChange?: (inStepView: boolean) => void; | |||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.1 | 2026-08-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.2 | 2026-10-29 */ | |||||
| const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| fromJosave, | fromJosave, | ||||
| onStepViewChange, | |||||
| }) => { | }) => { | ||||
| // console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); | // console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); | ||||
| @@ -143,6 +146,12 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| onBackRef.current = onBack; | onBackRef.current = onBack; | ||||
| }, [onBack]); | }, [onBack]); | ||||
| useEffect(() => { | |||||
| const inStepView = isExecutingLine && selectedLineId != null; | |||||
| onStepViewChange?.(inStepView); | |||||
| return () => onStepViewChange?.(false); | |||||
| }, [isExecutingLine, selectedLineId, onStepViewChange]); | |||||
| // 获取 process 和 lines 数据 | // 获取 process 和 lines 数据 | ||||
| const fetchProcessDetail = useCallback(async () => { | const fetchProcessDetail = useCallback(async () => { | ||||
| // console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); | // console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); | ||||
| @@ -54,7 +54,7 @@ interface ProductProcessJobOrderDetailProps { | |||||
| initialTabIndex?: number; | initialTabIndex?: number; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||||
| const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| @@ -68,6 +68,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | ||||
| const [tabIndex, setTabIndex] = useState(initialTabIndex); | const [tabIndex, setTabIndex] = useState(initialTabIndex); | ||||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | ||||
| const [inStepView, setInStepView] = useState(false); | |||||
| const [operationPriority, setOperationPriority] = useState<number>(50); | const [operationPriority, setOperationPriority] = useState<number>(50); | ||||
| const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | ||||
| const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); | const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); | ||||
| @@ -320,6 +321,9 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
| (_e, newValue) => { | (_e, newValue) => { | ||||
| setTabIndex(newValue); | setTabIndex(newValue); | ||||
| if (newValue !== 2) { | |||||
| setInStepView(false); | |||||
| } | |||||
| }, | }, | ||||
| [], | [], | ||||
| ); | ); | ||||
| @@ -796,12 +800,14 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| {/* 返回按钮 */} | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||||
| {t("Back to List")} | |||||
| </Button> | |||||
| </Box> | |||||
| {/* 步驟執行頁不顯示「返回列表」,避免誤離開工單 */} | |||||
| {!inStepView && ( | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||||
| {t("Back to List")} | |||||
| </Button> | |||||
| </Box> | |||||
| )} | |||||
| {/* 标签页 */} | {/* 标签页 */} | ||||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | ||||
| @@ -833,6 +839,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||||
| }} | }} | ||||
| fromJosave={Boolean(fromJosave && !isPutAwayed)} | fromJosave={Boolean(fromJosave && !isPutAwayed)} | ||||
| onStepViewChange={setInStepView} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | ||||
| @@ -29,6 +29,7 @@ import { | |||||
| import ChevronLeft from "@mui/icons-material/ChevronLeft"; | import ChevronLeft from "@mui/icons-material/ChevronLeft"; | ||||
| import ChevronRight from "@mui/icons-material/ChevronRight"; | import ChevronRight from "@mui/icons-material/ChevronRight"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import type { StockInLine } from "@/app/api/stockIn"; | |||||
| import { fetchItemForPutAway } from "@/app/api/stockIn/actions"; | import { fetchItemForPutAway } from "@/app/api/stockIn/actions"; | ||||
| import QcStockInModal from "../Qc/QcStockInModal"; | import QcStockInModal from "../Qc/QcStockInModal"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| @@ -178,7 +179,7 @@ function isWaitingQcPutAway( | |||||
| return s !== "completed" && s !== "rejected"; | return s !== "completed" && s !== "rejected"; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.9 | 2026-08-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ | const ProductProcessList: React.FC<ProductProcessListProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo, | printerCombo, | ||||
| @@ -191,6 +192,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| 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 [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| /** Bumps on Search/Reset so the same criteria still refetch. */ | |||||
| const [listReloadKey, setListReloadKey] = useState(0); | |||||
| const [productionCache, setProductionCache] = useState< | const [productionCache, setProductionCache] = useState< | ||||
| AllJoborderProductProcessInfoResponse[] | AllJoborderProductProcessInfoResponse[] | ||||
| >([]); | >([]); | ||||
| @@ -234,6 +237,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| () => printerOptions[0] ?? null, | () => printerOptions[0] ?? null, | ||||
| ); | ); | ||||
| const printInFlightRef = useRef(false); | const printInFlightRef = useRef(false); | ||||
| const loadSeqRef = useRef(0); | |||||
| const hasLoadedOnceRef = useRef(false); | |||||
| const [printingSilId, setPrintingSilId] = useState<number | null>(null); | const [printingSilId, setPrintingSilId] = useState<number | null>(null); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -328,6 +333,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| const handleApplySearch = useCallback( | const handleApplySearch = useCallback( | ||||
| (inputs: Record<SearchParam | `${SearchParam}To`, string>) => { | (inputs: Record<SearchParam | `${SearchParam}To`, string>) => { | ||||
| const selectedProcessType = (inputs.processType || "all") as ProcessFilter; | const selectedProcessType = (inputs.processType || "all") as ProcessFilter; | ||||
| setListReloadKey((k) => k + 1); | |||||
| onListPersistedStateChange((prev) => ({ | onListPersistedStateChange((prev) => ({ | ||||
| ...prev, | ...prev, | ||||
| filter: selectedProcessType, | filter: selectedProcessType, | ||||
| @@ -342,6 +348,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| ); | ); | ||||
| const handleResetSearch = useCallback(() => { | const handleResetSearch = useCallback(() => { | ||||
| setListReloadKey((k) => k + 1); | |||||
| onListPersistedStateChange((prev) => ({ | onListPersistedStateChange((prev) => ({ | ||||
| ...prev, | ...prev, | ||||
| filter: "all", | filter: "all", | ||||
| @@ -355,8 +362,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| }, [disableDateFilter, onListPersistedStateChange]); | }, [disableDateFilter, onListPersistedStateChange]); | ||||
| /** Load all tab datasets once per search; tab switches use cache only. */ | /** Load all tab datasets once per search; tab switches use cache only. */ | ||||
| const loadAllCaches = useCallback(async () => { | |||||
| setLoading(true); | |||||
| const loadAllCaches = useCallback(async (options?: { silent?: boolean }) => { | |||||
| const silent = options?.silent === true; | |||||
| const seq = ++loadSeqRef.current; | |||||
| if (!silent) setLoading(true); | |||||
| try { | try { | ||||
| const typeParam = filter === "all" ? undefined : filter; | const typeParam = filter === "all" ? undefined : filter; | ||||
| const base = { | const base = { | ||||
| @@ -391,6 +400,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| }), | }), | ||||
| ]); | ]); | ||||
| if (seq !== loadSeqRef.current) return; | |||||
| const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p)); | const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p)); | ||||
| const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p)); | const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p)); | ||||
| const putawayContent = putawayed?.content || []; | const putawayContent = putawayed?.content || []; | ||||
| @@ -405,6 +416,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| setCarriedOverCount(prod?.carriedOverCount ?? 0); | setCarriedOverCount(prod?.carriedOverCount ?? 0); | ||||
| } catch (e) { | } catch (e) { | ||||
| console.error(e); | console.error(e); | ||||
| if (seq !== loadSeqRef.current || silent) return; | |||||
| setProductionCache([]); | setProductionCache([]); | ||||
| setPendingQcCache([]); | setPendingQcCache([]); | ||||
| setPutawayedCache([]); | setPutawayedCache([]); | ||||
| @@ -413,12 +425,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| setPutawayedCount(0); | setPutawayedCount(0); | ||||
| setCarriedOverCount(0); | setCarriedOverCount(0); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | |||||
| if (seq === loadSeqRef.current) setLoading(false); | |||||
| } | } | ||||
| }, [appliedSearch, disableDateFilter, filter]); | |||||
| }, [appliedSearch, disableDateFilter, filter, listReloadKey]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| void loadAllCaches(); | |||||
| const silent = hasLoadedOnceRef.current; | |||||
| hasLoadedOnceRef.current = true; | |||||
| void loadAllCaches(silent ? { silent: true } : undefined); | |||||
| }, [loadAllCaches]); | }, [loadAllCaches]); | ||||
| const offPlanRows = useMemo(() => { | const offPlanRows = useMemo(() => { | ||||
| @@ -670,9 +684,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| closeConfirm(); | closeConfirm(); | ||||
| } | } | ||||
| }, [pendingConfirmAction, closeConfirm]); | }, [pendingConfirmAction, closeConfirm]); | ||||
| const closeNewModal = useCallback(() => { | |||||
| const closeNewModal = useCallback((updatedStockInLine?: StockInLine) => { | |||||
| setOpenModal(false); | setOpenModal(false); | ||||
| }, []); | |||||
| if (updatedStockInLine) { | |||||
| void loadAllCaches({ silent: true }); | |||||
| } | |||||
| }, [loadAllCaches]); | |||||
| const handlePrintLabel = useCallback( | const handlePrintLabel = useCallback( | ||||
| async (process: AllJoborderProductProcessInfoResponse) => { | async (process: AllJoborderProductProcessInfoResponse) => { | ||||
| @@ -28,6 +28,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||||
| import StopIcon from "@mui/icons-material/Stop"; | import StopIcon from "@mui/icons-material/Stop"; | ||||
| import PauseIcon from "@mui/icons-material/Pause"; | import PauseIcon from "@mui/icons-material/Pause"; | ||||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| JobOrderProcessLineDetailResponse, | JobOrderProcessLineDetailResponse, | ||||
| @@ -47,6 +48,35 @@ import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
| import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 | import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 | ||||
| import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 | import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| type QtyInput = number | ""; | |||||
| type OutputFormState = Omit< | |||||
| UpdateProductProcessLineQtyRequest, | |||||
| | "outputFromProcessQty" | |||||
| | "byproductQty" | |||||
| | "defectQty" | |||||
| | "defect2Qty" | |||||
| | "defect3Qty" | |||||
| | "scrapQty" | |||||
| > & { | |||||
| outputFromProcessQty: QtyInput; | |||||
| byproductQty: QtyInput; | |||||
| defectQty: QtyInput; | |||||
| defect2Qty: QtyInput; | |||||
| defect3Qty: QtyInput; | |||||
| scrapQty: QtyInput; | |||||
| }; | |||||
| const parseQtyInput = (raw: string): QtyInput => { | |||||
| if (raw === "") return ""; | |||||
| const n = parseInt(raw, 10); | |||||
| return Number.isNaN(n) ? "" : n; | |||||
| }; | |||||
| const toSubmitQty = (v: QtyInput | null | undefined): number => | |||||
| v === "" || v == null || Number.isNaN(Number(v)) ? 0 : Number(v); | |||||
| interface ProductionProcessStepExecutionProps { | interface ProductionProcessStepExecutionProps { | ||||
| lineId: number | null; | lineId: number | null; | ||||
| onBack: () => void; | onBack: () => void; | ||||
| @@ -55,7 +85,7 @@ interface ProductionProcessStepExecutionProps { | |||||
| jobOrderId?: number; // ✅ 添加 | jobOrderId?: number; // ✅ 添加 | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.0 | 2026-08-10 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | ||||
| lineId, | lineId, | ||||
| onBack, | onBack, | ||||
| @@ -75,24 +105,20 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| lineStatusNorm === "autopass"; | lineStatusNorm === "autopass"; | ||||
| const isPassStatus = lineStatusNorm === "pass"; | const isPassStatus = lineStatusNorm === "pass"; | ||||
| const isAutoPassStatus = lineStatusNorm === "autopass"; | const isAutoPassStatus = lineStatusNorm === "autopass"; | ||||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||||
| byproductName: string; | |||||
| byproductQty: number; | |||||
| byproductUom: string; | |||||
| }>({ | |||||
| const [outputData, setOutputData] = useState<OutputFormState>({ | |||||
| productProcessLineId: lineId ?? 0, | productProcessLineId: lineId ?? 0, | ||||
| outputFromProcessQty: 0, | |||||
| outputFromProcessQty: "", | |||||
| outputFromProcessUom: "", | outputFromProcessUom: "", | ||||
| defectQty: 0, | |||||
| defectQty: "", | |||||
| defectUom: "", | defectUom: "", | ||||
| scrapQty: 0, | |||||
| scrapQty: "", | |||||
| scrapUom: "", | scrapUom: "", | ||||
| byproductName: "", | byproductName: "", | ||||
| byproductQty: 0, | |||||
| byproductQty: "", | |||||
| byproductUom: "", | byproductUom: "", | ||||
| defect2Qty: 0, | |||||
| defect2Qty: "", | |||||
| defect2Uom: "", | defect2Uom: "", | ||||
| defect3Qty: 0, | |||||
| defect3Qty: "", | |||||
| defect3Uom: "", | defect3Uom: "", | ||||
| defectDescription: "", | defectDescription: "", | ||||
| defectDescription2: "", | defectDescription2: "", | ||||
| @@ -153,14 +179,14 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| setOutputData(prev => ({ | setOutputData(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | productProcessLineId: detail.id, | ||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | outputFromProcessUom: (detail as any).outputFromProcessUom || "", | ||||
| defectQty: detail.defectQty || 0, | |||||
| defectQty: detail.defectQty || "", | |||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| scrapQty: detail.scrapQty || 0, | |||||
| scrapQty: detail.scrapQty || "", | |||||
| scrapUom: detail.scrapUom || "", | scrapUom: detail.scrapUom || "", | ||||
| byproductName: detail.byproductName || "", | byproductName: detail.byproductName || "", | ||||
| byproductQty: detail.byproductQty || 0, | |||||
| byproductQty: detail.byproductQty || "", | |||||
| byproductUom: detail.byproductUom || "" | byproductUom: detail.byproductUom || "" | ||||
| })); | })); | ||||
| }) | }) | ||||
| @@ -351,20 +377,20 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| await updateProductProcessLineQty({ | await updateProductProcessLineQty({ | ||||
| productProcessLineId: lineDetail?.id || 0 as number, | productProcessLineId: lineDetail?.id || 0 as number, | ||||
| byproductName: outputData.byproductName, | byproductName: outputData.byproductName, | ||||
| byproductQty: outputData.byproductQty, | |||||
| byproductQty: toSubmitQty(outputData.byproductQty), | |||||
| byproductUom: outputData.byproductUom, | byproductUom: outputData.byproductUom, | ||||
| outputFromProcessQty: outputData.outputFromProcessQty, | |||||
| outputFromProcessQty: toSubmitQty(outputData.outputFromProcessQty), | |||||
| outputFromProcessUom: outputData.outputFromProcessUom, | outputFromProcessUom: outputData.outputFromProcessUom, | ||||
| defectQty: outputData.defectQty, | |||||
| defectQty: toSubmitQty(outputData.defectQty), | |||||
| defectUom: outputData.defectUom, | defectUom: outputData.defectUom, | ||||
| defect2Qty: outputData.defect2Qty, | |||||
| defect2Qty: toSubmitQty(outputData.defect2Qty), | |||||
| defect2Uom: outputData.defect2Uom, | defect2Uom: outputData.defect2Uom, | ||||
| defect3Qty: outputData.defect3Qty, | |||||
| defect3Qty: toSubmitQty(outputData.defect3Qty), | |||||
| defect3Uom: outputData.defect3Uom, | defect3Uom: outputData.defect3Uom, | ||||
| defectDescription: outputData.defectDescription, | defectDescription: outputData.defectDescription, | ||||
| defectDescription2: outputData.defectDescription2, | defectDescription2: outputData.defectDescription2, | ||||
| defectDescription3: outputData.defectDescription3, | defectDescription3: outputData.defectDescription3, | ||||
| scrapQty: outputData.scrapQty, | |||||
| scrapQty: toSubmitQty(outputData.scrapQty), | |||||
| scrapUom: outputData.scrapUom, | scrapUom: outputData.scrapUom, | ||||
| }); | }); | ||||
| @@ -383,21 +409,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| setOutputData(prev => ({ | setOutputData(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | productProcessLineId: detail.id, | ||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | outputFromProcessUom: (detail as any).outputFromProcessUom || "", | ||||
| defectQty: detail.defectQty || 0, | |||||
| defectQty: detail.defectQty || "", | |||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| defectDescription: detail.defectDescription || "", | defectDescription: detail.defectDescription || "", | ||||
| defectDescription2: detail.defectDescription2 || "", | defectDescription2: detail.defectDescription2 || "", | ||||
| defectDescription3: detail.defectDescription3 || "", | defectDescription3: detail.defectDescription3 || "", | ||||
| defectQty2: detail.defectQty2 || 0, | |||||
| defectUom2: detail.defectUom2 || "", | |||||
| defectQty3: detail.defectQty3 || 0, | |||||
| defectUom3: detail.defectUom3 || "", | |||||
| scrapQty: detail.scrapQty || 0, | |||||
| defect2Qty: (detail as any).defect2Qty || "", | |||||
| defect2Uom: (detail as any).defect2Uom || "", | |||||
| defect3Qty: (detail as any).defect3Qty || "", | |||||
| defect3Uom: (detail as any).defect3Uom || "", | |||||
| scrapQty: detail.scrapQty || "", | |||||
| scrapUom: detail.scrapUom || "", | scrapUom: detail.scrapUom || "", | ||||
| byproductName: detail.byproductName || "", | byproductName: detail.byproductName || "", | ||||
| byproductQty: detail.byproductQty || 0, | |||||
| byproductQty: detail.byproductQty || "", | |||||
| byproductUom: detail.byproductUom || "" | byproductUom: detail.byproductUom || "" | ||||
| })); | })); | ||||
| }) | }) | ||||
| @@ -561,8 +587,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <Button variant="outlined" onClick={onBack}> | |||||
| {t("Back to List")} | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={onBack} | |||||
| startIcon={<ArrowBackIcon />} | |||||
| > | |||||
| {t("Back to Process Steps")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| {processData && ( | {processData && ( | ||||
| @@ -812,7 +843,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| value={outputData.outputFromProcessQty} | value={outputData.outputFromProcessQty} | ||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| outputFromProcessQty: parseInt(e.target.value) || 0 | |||||
| outputFromProcessQty: parseQtyInput(e.target.value) | |||||
| })} | })} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -854,7 +885,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| value={outputData.defectQty} | value={outputData.defectQty} | ||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defectQty: parseInt(e.target.value) || 0 | |||||
| defectQty: parseQtyInput(e.target.value) | |||||
| })} | })} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -902,7 +933,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| value={outputData.defect2Qty} | value={outputData.defect2Qty} | ||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defect2Qty: parseInt(e.target.value) || 0 | |||||
| defect2Qty: parseQtyInput(e.target.value) | |||||
| })} | })} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -950,7 +981,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| value={outputData.defect3Qty} | value={outputData.defect3Qty} | ||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defect3Qty: parseInt(e.target.value) || 0 | |||||
| defect3Qty: parseQtyInput(e.target.value) | |||||
| })} | })} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -998,7 +1029,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| value={outputData.scrapQty} | value={outputData.scrapQty} | ||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| scrapQty: parseInt(e.target.value) || 0 | |||||
| scrapQty: parseQtyInput(e.target.value) | |||||
| })} | })} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -77,7 +77,7 @@ interface Props extends CommonProps { | |||||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.7 | 2026-10-15 */ | |||||
| const QcStockInModal: React.FC<Props> = ({ | const QcStockInModal: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -617,10 +617,10 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| } | } | ||||
| closeWithResult(qcRes); | |||||
| closeWithResult(qcRes ?? stockInLineInfo); | |||||
| // setTabIndex(1); // Need to go Putaway tab? | // setTabIndex(1); // Need to go Putaway tab? | ||||
| } else { | } else { | ||||
| closeWithResult(qcRes); | |||||
| closeWithResult(qcRes ?? stockInLineInfo); | |||||
| } | } | ||||
| setIsSubmitting(false); | setIsSubmitting(false); | ||||
| qcSubmitInFlightRef.current = false; | qcSubmitInFlightRef.current = false; | ||||
| @@ -700,11 +700,12 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| // Handle putaway submission logic here | // Handle putaway submission logic here | ||||
| const res = await postStockInLine(putawayData); | const res = await postStockInLine(putawayData); | ||||
| console.log("result ", res); | console.log("result ", res); | ||||
| // Close modal after successful putaway | |||||
| closeHandler({}, "backdropClick"); | |||||
| const updated = Array.isArray(res.entity) ? res.entity[0] : res.entity; | |||||
| // Close modal after successful putaway and tell the parent to refresh. | |||||
| closeWithResult(updated ?? stockInLineInfo); | |||||
| }, | }, | ||||
| [closeHandler], | |||||
| [closeWithResult, postStockInLine, stockInLineInfo], | |||||
| ); | ); | ||||
| // Print handler | // Print handler | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -32,6 +32,8 @@ | |||||
| "BOM Type": "BOM Type", | "BOM Type": "BOM Type", | ||||
| "Back": "Back", | "Back": "Back", | ||||
| "Back to List": "Back to List", | "Back to List": "Back to List", | ||||
| "Back to Process": "Back to Process Steps", | |||||
| "Back to Process Steps": "Back to Process Steps", | |||||
| "Bad Item Qty": "Bad Item Qty", | "Bad Item Qty": "Bad Item Qty", | ||||
| "Bad Package Qty": "Bad Package Qty", | "Bad Package Qty": "Bad Package Qty", | ||||
| "Bag": "Bag", | "Bag": "Bag", | ||||
| @@ -10,6 +10,8 @@ | |||||
| "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", | "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", | ||||
| "Back": "Back", | "Back": "Back", | ||||
| "Back to List": "Back to List", | "Back to List": "Back to List", | ||||
| "Back to Process": "Back to Process Steps", | |||||
| "Back to Process Steps": "Back to Process Steps", | |||||
| "Bag": "Bag", | "Bag": "Bag", | ||||
| "Bag Consumption": "Bag Consumption", | "Bag Consumption": "Bag Consumption", | ||||
| "Balance": "Balance", | "Balance": "Balance", | ||||
| @@ -106,7 +106,7 @@ | |||||
| "lane_noMatchFilter": "No lanes match (clear search or floor filter)", | "lane_noMatchFilter": "No lanes match (clear search or floor filter)", | ||||
| "floor_label": "Floor", | "floor_label": "Floor", | ||||
| "floor_all": "All", | "floor_all": "All", | ||||
| "filter_clear": "Clear", | |||||
| "filter_clear": "Clear all", | |||||
| "filter_apply": "OK", | "filter_apply": "OK", | ||||
| "btn_addLane": "Add lane", | "btn_addLane": "Add lane", | ||||
| "tools_title": "Tools", | "tools_title": "Tools", | ||||
| @@ -132,8 +132,26 @@ | |||||
| "confirm_restoreDiscardsEdits": "Scheduling a version restore will discard other unsaved board changes (drags, deletes, pending shops/lanes, logistics fields, etc.). Continue?", | "confirm_restoreDiscardsEdits": "Scheduling a version restore will discard other unsaved board changes (drags, deletes, pending shops/lanes, logistics fields, etc.). Continue?", | ||||
| "confirm_restoreSaveWillDropStaging": "Save will apply the snapshot restore first; other staged edits in this save will be skipped. Continue?", | "confirm_restoreSaveWillDropStaging": "Save will apply the snapshot restore first; other staged edits in this save will be skipped. Continue?", | ||||
| "confirm_schedule_removeShop": "Remove this shop from the lane? It will be scheduled for deletion when you submit.", | "confirm_schedule_removeShop": "Remove this shop from the lane? It will be scheduled for deletion when you submit.", | ||||
| "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", | |||||
| "departureDialog_title": "Edit departure time", | |||||
| "departureDialog_hint": "Name and departure apply to every master row on this lane. Press \"Save changes\" to write them. Existing delivery tickets keep the old name; later releases use the new name.", | |||||
| "departureDialog_title": "Edit lane", | |||||
| "departureDialog_nameLabel": "Lane name", | |||||
| "laneEditTooltip": "Edit lane name and departure time (written when you press Save changes)", | |||||
| "confirm_weekdayToken": "The weekday token in the lane name will change or become unreadable. 4F assignment matches Mon–Sun inside the name. Apply anyway?", | |||||
| "confirm_renameLane": "Rename \"{{from}}\" to \"{{to}}\".\n{{unfinished}} unfinished delivery ticket(s) stay on the old name (handlers: {{handlers}}).\n{{completed}} completed ticket(s) stay on the old name.\nLater releases use the new name.\n{{schedules}} open schedule line(s) will change from/to.\nNothing is written until you press Save changes.", | |||||
| "confirm_restoreKeepsTickets": "Restore puts lane master data, and open schedules that point at the current name, back to this snapshot. Unfinished delivery tickets stay on the current name.", | |||||
| "rename_noHandlers": "none", | |||||
| "rename_none": "none", | |||||
| "rename_savePartial": "Renamed: {{ok}}. Failed: {{fail}}. The failed lane was not written. Successful lanes stay renamed. Refresh, then save again.", | |||||
| "renameErr_preview": "Could not load ticket and schedule counts, so this edit was not applied.", | |||||
| "renameErr_LANE_NAME_EMPTY": "Lane name cannot be blank", | |||||
| "renameErr_LANE_NAME_TOO_LONG": "Lane name is limited to 50 characters", | |||||
| "renameErr_LANE_NAME_RESERVED": "\"車線-X\" cannot be used or renamed", | |||||
| "renameErr_LANE_NAME_TAKEN": "This floor and remark already has that lane", | |||||
| "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "This floor already has the same name with a different remark. Delivery tickets cannot tell them apart.", | |||||
| "renameErr_LANE_NOT_FOUND": "No live lane rows were found, so nothing was written", | |||||
| "renameErr_LANE_TIME_INVALID": "Departure time is not a valid time", | |||||
| "renameLog_title": "Lane renames", | |||||
| "renameLog_line": "{{when}} {{who}}: {{from}} → {{to}} ({{floor}})", | |||||
| "departureEditAria": "Edit departure time", | "departureEditAria": "Edit departure time", | ||||
| "departureTooltipEditSave": "Edit departure time (saved with \"Save changes\")", | "departureTooltipEditSave": "Edit departure time (saved with \"Save changes\")", | ||||
| "departureTooltipNeedShops": "Add shops before setting departure time", | "departureTooltipNeedShops": "Add shops before setting departure time", | ||||
| @@ -212,9 +230,14 @@ | |||||
| "err_save": "Save failed", | "err_save": "Save failed", | ||||
| "exportRoutes": "Export routes", | "exportRoutes": "Export routes", | ||||
| "filter_apply": "OK", | "filter_apply": "OK", | ||||
| "filter_clear": "Clear", | |||||
| "filter_clear": "Clear all", | |||||
| "floor_all": "All", | "floor_all": "All", | ||||
| "floor_label": "Floor", | "floor_label": "Floor", | ||||
| "lane_filter": "Filter", | |||||
| "laneFilter_special": "Lane code", | |||||
| "laneFilter_zone": "Zone", | |||||
| "laneFilter_weekday": "Weekday", | |||||
| "laneFilter_area": "Area", | |||||
| "id": "id", | "id": "id", | ||||
| "importRoutes": "Import routes", | "importRoutes": "Import routes", | ||||
| "import_staged_preview": "Import preview loaded: {{file}} ({{sheets}} sheet(s) / {{rows}} rows). Press \"Save changes\" to persist.", | "import_staged_preview": "Import preview loaded: {{file}} ({{sheets}} sheet(s) / {{rows}} rows). Press \"Save changes\" to persist.", | ||||
| @@ -34,6 +34,8 @@ | |||||
| "BOM Type": "BOM 類型", | "BOM Type": "BOM 類型", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "Back to List": "返回列表", | "Back to List": "返回列表", | ||||
| "Back to Process": "返回列表", | |||||
| "Back to Process Steps": "返回列表", | |||||
| "Bad Item Qty": "不良物品數量", | "Bad Item Qty": "不良物品數量", | ||||
| "Bad Package Qty": "不良包裝數量", | "Bad Package Qty": "不良包裝數量", | ||||
| "Bag": "包裝袋", | "Bag": "包裝袋", | ||||
| @@ -10,6 +10,8 @@ | |||||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "Back to List": "返回列表", | "Back to List": "返回列表", | ||||
| "Back to Process": "返回列表", | |||||
| "Back to Process Steps": "返回列表", | |||||
| "Bag": "包裝袋", | "Bag": "包裝袋", | ||||
| "Bag Consumption": "包裝袋消耗", | "Bag Consumption": "包裝袋消耗", | ||||
| "Balance": "可用數量", | "Balance": "可用數量", | ||||
| @@ -106,7 +106,7 @@ | |||||
| "lane_noMatchFilter": "無符合條件的車線(清除搜索或樓層篩選)", | "lane_noMatchFilter": "無符合條件的車線(清除搜索或樓層篩選)", | ||||
| "floor_label": "樓層", | "floor_label": "樓層", | ||||
| "floor_all": "全部", | "floor_all": "全部", | ||||
| "filter_clear": "清除", | |||||
| "filter_clear": "清除全部", | |||||
| "filter_apply": "確定", | "filter_apply": "確定", | ||||
| "btn_addLane": "新增車線", | "btn_addLane": "新增車線", | ||||
| "tools_title": "操作工具", | "tools_title": "操作工具", | ||||
| @@ -106,7 +106,12 @@ | |||||
| "lane_selectAll": "全選", | "lane_selectAll": "全選", | ||||
| "floor_label": "樓層", | "floor_label": "樓層", | ||||
| "floor_all": "全部", | "floor_all": "全部", | ||||
| "filter_clear": "清除", | |||||
| "lane_filter": "篩選", | |||||
| "laneFilter_special": "車線", | |||||
| "laneFilter_zone": "區", | |||||
| "laneFilter_weekday": "星期", | |||||
| "laneFilter_area": "地區", | |||||
| "filter_clear": "清除全部", | |||||
| "filter_apply": "確定", | "filter_apply": "確定", | ||||
| "btn_addLane": "新增車線", | "btn_addLane": "新增車線", | ||||
| "tools_title": "操作工具", | "tools_title": "操作工具", | ||||
| @@ -200,8 +205,26 @@ | |||||
| "dialog_editLogisticsTitle": "編輯物流公司", | "dialog_editLogisticsTitle": "編輯物流公司", | ||||
| "btn_apply": "套用", | "btn_apply": "套用", | ||||
| "addShop_confirm": "確認", | "addShop_confirm": "確認", | ||||
| "departureDialog_title": "編輯出車時間", | |||||
| "departureDialog_hint": "套用至此車線所有店鋪列。", | |||||
| "departureDialog_title": "編輯車線", | |||||
| "departureDialog_nameLabel": "車線名稱", | |||||
| "departureDialog_hint": "名稱與出車時間會套用到此車線全部店鋪。\n 既有送貨訂單維持舊名稱;之後放單才用新名稱。\n 按上方「儲存更改」才寫入。", | |||||
| "laneEditTooltip": "編輯車線名稱與出車時間(按「儲存更改」才寫入)", | |||||
| "confirm_weekdayToken": "車線名稱裡的星期代碼會改變,或改完後無法辨認。4F 派車是用名稱裡的 Mon–Sun 對日子。仍要套用?", | |||||
| "confirm_renameLane": "把「{{from}}」改成「{{to}}」。\n未完成出倉票 {{unfinished}} 張維持舊名稱(負責人:{{handlers}})。\n已完成 {{completed}} 張維持舊名稱。\n之後放單才用新名稱。\n未完成排程 {{schedules}} 筆的起迄名稱會改。\n按「儲存更改」後才寫入。", | |||||
| "confirm_restoreKeepsTickets": "還原會把車線主檔,以及指向目前名稱的未完成排程,改回此版本的名稱。未完成的出倉票會留在現在的名稱,不會跟回去。", | |||||
| "rename_noHandlers": "無", | |||||
| "rename_none": "無", | |||||
| "rename_savePartial": "已改名:{{ok}}。失敗:{{fail}}。失敗的那一桶沒有寫入;已經成功的不會自動改回去。請重新整理後再存。", | |||||
| "renameErr_preview": "讀不到出倉票與排程數量,這次沒有套用。", | |||||
| "renameErr_LANE_NAME_EMPTY": "車線名稱不可空白", | |||||
| "renameErr_LANE_NAME_TOO_LONG": "車線名稱最多 50 字", | |||||
| "renameErr_LANE_NAME_RESERVED": "不能使用或改掉「車線-X」", | |||||
| "renameErr_LANE_NAME_TAKEN": "同一樓層、同一備註已有這條車線", | |||||
| "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "同一樓層已有相同名稱但備註不同,出倉票無法分辨,已拒絕", | |||||
| "renameErr_LANE_NOT_FOUND": "找不到這條車線的主檔,沒有寫入", | |||||
| "renameErr_LANE_TIME_INVALID": "出車時間格式不正確", | |||||
| "renameLog_title": "車線改名紀錄", | |||||
| "renameLog_line": "{{when}} {{who}}:{{from}} → {{to}}({{floor}})", | |||||
| "seqDialog_title": "編輯裝車順序", | "seqDialog_title": "編輯裝車順序", | ||||
| "logistics_colLaneCount": "{{count}} 條車線", | "logistics_colLaneCount": "{{count}} 條車線", | ||||
| "tooltip_openLaneBoard": "在車線看板開此車線", | "tooltip_openLaneBoard": "在車線看板開此車線", | ||||