diff --git a/src/app/(main)/jo/edit/page.tsx b/src/app/(main)/jo/edit/page.tsx index 29b9c7f..6868224 100644 --- a/src/app/(main)/jo/edit/page.tsx +++ b/src/app/(main)/jo/edit/page.tsx @@ -25,10 +25,13 @@ const JoEdit: React.FC = async ({ searchParams }) => { try { await fetchJoDetail(parseInt(id)) } catch (e) { + if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { - console.log(e) - notFound(); + console.log("Job Order not found:", e); + } else { + console.error("Error fetching Job Order detail:", e); } + notFound(); } diff --git a/src/app/api/bom/index.ts b/src/app/api/bom/index.ts index 9e8e3ff..c96c52b 100644 --- a/src/app/api/bom/index.ts +++ b/src/app/api/bom/index.ts @@ -8,6 +8,7 @@ export interface BomCombo { label: string; outputQty: number; outputQtyUom: string; + description: string; } export const preloadBomCombo = (() => { diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index 6f70823..5d54928 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -173,9 +173,9 @@ export const fetchDoRecordByPage = cache(async (data?: SearchDeliveryOrderInfoRe return response }) -export const fetchTicketReleaseTable = cache(async ()=> { +export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: string)=> { return await serverFetchJson( - `${BASE_API_URL}/doPickOrder/ticket-release-table`, + `${BASE_API_URL}/doPickOrder/ticket-release-table/${startDate}&${endDate}`, { method: "GET", } diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 058c217..c291f92 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -1,11 +1,12 @@ "use server"; import { cache } from 'react'; -import { Pageable, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { Pageable, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { JobOrder, JoStatus, Machine, Operator } from "."; import { BASE_API_URL } from "@/config/api"; import { revalidateTag } from "next/cache"; import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; +import { FileResponse } from "@/app/api/pdf/actions"; export interface SaveJo { bomId: number; @@ -155,7 +156,7 @@ export const printFGStockInLabel = cache(async(data: PrintFGStockInLabelRequest) } return serverFetchWithNoContent( - `${BASE_API_URL}/jo/print-FGPickRecordLabel?${params.toString()}`, + `${BASE_API_URL}/jo/print-FGStockInLabel?${params.toString()}`, { method: "GET", next: { @@ -214,6 +215,7 @@ export interface ProductProcessLineResponse { seqNo: number, name: string, description: string, + equipmentDetailId: number, equipment_name: string, equipmentDetailCode: string, status: string, @@ -259,6 +261,7 @@ export interface ProductProcessWithLinesResponse { outputQtyUom: string; productionPriority: number; jobOrderLines: JobOrderLineInfo[]; + productProcessLines: ProductProcessLineResponse[]; } @@ -320,9 +323,12 @@ export interface AllJoborderProductProcessInfoResponse { bomId?: number; assignedTo: number; pickOrderId: number; + pickOrderStatus: string; + itemCode: string; itemName: string; requiredQty: number; jobOrderId: number; + uom: string; stockInLineId: number; jobOrderCode: string; productProcessLineCount: number; @@ -346,6 +352,11 @@ export interface ProductProcessLineQrscanUpadteRequest { equipmentTypeSubTypeEquipmentNo?: string; staffNo?: string; } +export interface NewProductProcessLineQrscanUpadteRequest{ + productProcessLineId: number; + equipmentCode?: string; + staffNo?: string; +} export interface ProductProcessLineDetailResponse { id: number, @@ -398,7 +409,9 @@ export interface JobOrderProcessLineDetailResponse { description: string; equipmentId: number; startTime: string | number[]; // API 返回的是数组格式 - endTime: string | number[]; // API 返回的是数组格式 + endTime: string | number[]; + stopTime: string | number[]; + totalPausedTimeMs?: number; // API 返回的是数组格式 status: string; outputFromProcessQty: number; outputFromProcessUom: string; @@ -524,6 +537,7 @@ export interface PickOrderLineWithLotsResponse { uomCode: string | null; uomDesc: string | null; status: string | null; + handler: string | null; lots: LotDetailResponse[]; } @@ -554,7 +568,16 @@ export interface LotDetailResponse { matchQty?: number | null; } - +export interface JobOrderListForPrintQrCodeResponse { + id: number; + code: string; + name: string; + reqQty: number; + stockOutLineId: number; + stockOutLineQty: number; + stockOutLineStatus: string; + finihedTime: string; +} export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { return serverFetchJson( `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, @@ -656,6 +679,18 @@ export const updateProductProcessLineQrscan = cache(async (request: ProductProce } ); }); + + +export const newUpdateProductProcessLineQrscan = cache(async (request: NewProductProcessLineQrscanUpadteRequest) => { + return serverFetchJson( + `${BASE_API_URL}/product-process/Demo/NewUpdate`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + } + ); +}); export const fetchAllJoborderProductProcessInfo = cache(async () => { return serverFetchJson( `${BASE_API_URL}/product-process/Demo/Process/all`, @@ -868,16 +903,24 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => ); }); // 获取已完成的 Job Order pick orders -export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => { +export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { return serverFetchJson( - `${BASE_API_URL}/jo/completed-job-order-pick-orders-only/${userId}`, + `${BASE_API_URL}/jo/completed-job-order-pick-orders-only`, { method: "GET", next: { tags: ["jo-completed"] }, }, ); }); - +export const fetchJoForPrintQrCode = cache(async (date: string) => { + return serverFetchJson( + `${BASE_API_URL}/jo/joForPrintQrCode/${date}`, + { + method: "GET", + next: { tags: ["jo-print-qr-code"] }, + }, + ); +}); // 获取已完成的 Job Order pick order records export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { return serverFetchJson( @@ -1027,3 +1070,20 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ return { success: true, message: "Print job sent successfully (Pick Record)" } as PrintPickRecordResponse; } + +export interface ExportFGStockInLabelRequest { + stockInLineId: number; +} + +export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Promise => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/jo/FGStockInLabel`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob; +}; \ No newline at end of file diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 729c998..14c891d 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -39,6 +39,10 @@ export interface ReleaseProdScheduleInputs { demandQty: number; } +export interface ReleaseProdScheduleReq { + id: number; +} + export interface ReleaseProdScheduleResponse { id: number; code: string; @@ -48,6 +52,12 @@ export interface ReleaseProdScheduleResponse { message: string; } +export interface ReleaseProdScheduleRsp { + id: number; + code: string; + message: string; +} + export interface SaveProdScheduleResponse { id: number; code: string; @@ -151,6 +161,41 @@ export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInp return response; }) +export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => { + const response = serverFetchJson( + `${BASE_API_URL}/productionSchedule/detail/detailed/release`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + //revalidateTag("detailedProdSchedules"); + //revalidateTag("prodSchedule"); + + return response; +}) + +export const exportProdSchedule = async (token: string | null) => { + if (!token) throw new Error("No access token found"); + + const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, { + method: "POST", + headers: { + "Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error(`Backend error: ${response.status}`); + + const arrayBuffer = await response.arrayBuffer(); + + // Convert to Base64 so Next.js can send it safely over the wire + return Buffer.from(arrayBuffer).toString('base64'); +}; + export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { const response = serverFetchJson( `${BASE_API_URL}/productionSchedule/detail/detailed/save`, diff --git a/src/app/api/scheduling/index.ts b/src/app/api/scheduling/index.ts index 482ddf5..4171c56 100644 --- a/src/app/api/scheduling/index.ts +++ b/src/app/api/scheduling/index.ts @@ -100,6 +100,13 @@ export interface DetailedProdScheduleLineResult { priority: number; approved: boolean; proportion: number; + lastMonthAvgSales: number; + avgQtyLastMonth: number; // Average usage last month + stockQty: number; // Warehouse stock quantity + daysLeft: number; // Days remaining before stockout + needNoOfJobOrder: number; + prodQty: number; + outputQty: number; } export interface DetailedProdScheduleLineBomMaterialResult { diff --git a/src/app/api/stockIn/index.ts b/src/app/api/stockIn/index.ts index 2cafaca..c1655d5 100644 --- a/src/app/api/stockIn/index.ts +++ b/src/app/api/stockIn/index.ts @@ -128,6 +128,7 @@ export interface StockInLine { dnNo?: string; dnDate?: number[]; stockQty?: number; + bomDescription?: string; handlerId?: number; putAwayLines?: PutAwayLine[]; qcResult?: QcResult[]; diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index 2848949..12e17c3 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -12,6 +12,7 @@ import { SearchProdSchedule, fetchDetailedProdSchedules, fetchProdSchedules, + exportProdSchedule, testDetailedSchedule, } from "@/app/api/scheduling/actions"; import { defaultPagingController } from "../SearchResults/SearchResults"; @@ -21,6 +22,7 @@ import { orderBy, uniqBy, upperFirst } from "lodash"; import { Button, Stack } from "@mui/material"; import isToday from 'dayjs/plugin/isToday'; import useUploadContext from "../UploadProvider/useUploadContext"; +import { FileDownload, CalendarMonth } from "@mui/icons-material"; dayjs.extend(isToday); // may need move to "index" or "actions" @@ -77,17 +79,17 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { // type: "dateRange", // }, { label: t("Production Date"), paramName: "scheduleAt", type: "date" }, - { - label: t("Product Count"), - paramName: "totalEstProdCount", - type: "text", - }, - { - label: t("Type"), - paramName: "types", - type: "autocomplete", - options: typeOptions, - }, + //{ + // label: t("Product Count"), + // paramName: "totalEstProdCount", + // type: "text", + //}, + //{ + // label: t("Type"), + // paramName: "types", + // type: "autocomplete", + // options: typeOptions, + //}, ]; return searchCriteria; }, [t]); @@ -177,18 +179,18 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { ) as ScheduleType[]; const params: SearchProdSchedule = { - scheduleAt: dayjs(query?.scheduleAt).isValid() - ? query?.scheduleAt - : undefined, - schedulePeriod: dayjs(query?.schedulePeriod).isValid() - ? query?.schedulePeriod - : undefined, - schedulePeriodTo: dayjs(query?.schedulePeriodTo).isValid() - ? query?.schedulePeriodTo - : undefined, - totalEstProdCount: query?.totalEstProdCount - ? Number(query?.totalEstProdCount) - : undefined, + //scheduleAt: dayjs(query?.scheduleAt).isValid() + // ? query?.scheduleAt + // : undefined, + //schedulePeriod: dayjs(query?.schedulePeriod).isValid() + // ? query?.schedulePeriod + // : undefined, + //schedulePeriodTo: dayjs(query?.schedulePeriodTo).isValid() + // ? query?.schedulePeriodTo + // : undefined, + //totalEstProdCount: query?.totalEstProdCount + // ? Number(query?.totalEstProdCount) + // : undefined, types: convertedTypes, pageNum: pagingController.pageNum - 1, pageSize: pagingController.pageSize, @@ -207,7 +209,7 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { setFilteredSchedules((fs) => orderBy( uniqBy([...fs, ...response.records], "id"), - ["id"], ["desc"])); + ["id"], ["asc"])); break; } } @@ -298,20 +300,67 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { } }, [inputs]) + const exportProdScheduleClick = async () => { + try { + const token = localStorage.getItem("accessToken"); + // 1. Get Base64 string from server + const base64String = await exportProdSchedule(token); + + // 2. Convert Base64 back to Blob + const byteCharacters = atob(base64String); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + const blob = new Blob([byteArray], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + }); + + // 3. Trigger download (same as before) + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "production_schedule.xlsx"; + link.click(); + window.URL.revokeObjectURL(url); + + } catch (error) { + console.error(error); + alert("Export failed. Check the console for details."); + } + }; + return ( <> + + = ({ // console.log(type) const apiRef = useGridApiRef(); const params = useSearchParams(); - console.log(params.get("id")); + const scheduleId = params.get("id"); // Get the schedule ID for the global release API + console.log(scheduleId); const [serverError, setServerError] = useState(""); const [tabIndex, setTabIndex] = useState(0); const { t } = useTranslation("schedule"); @@ -72,6 +75,14 @@ const DetailedScheduleDetailView: React.FC = ({ }); const errors = formProps.formState.errors; + const { reset, handleSubmit, setValue, getValues } = formProps + + useEffect(() => { + if (defaultValues) { + reset(defaultValues); + } + }, [defaultValues, reset]); + const lineFormProps = useFieldArray({ control: formProps.control, name: "prodScheduleLines" @@ -138,32 +149,64 @@ const DetailedScheduleDetailView: React.FC = ({ }) if (response) { - const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) - // console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`)) - // formProps.setValue(`prodScheduleLines.${index}.approved`, true) - // formProps.setValue(`prodScheduleLines.${index}.jobNo`, response.code) + // Find index of the updated line to refresh its data + // const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) + + // Update the entire line array, assuming the backend returns the updated list formProps.setValue(`prodScheduleLines`, response.entity.prodScheduleLines.sort((a, b) => b.priority - a.priority)) - // console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`)) } setIsUploading(false) } catch (e) { console.log(e) setIsUploading(false) } - }, []) + }, [formProps, setIsUploading]) + + // --- NEW FUNCTION: GLOBAL RELEASE FOR THE ENTIRE SCHEDULE --- + const onGlobalReleaseClick = useCallback(async () => { + if (!scheduleId) { + setServerError(t("Cannot release. Schedule ID is missing.")); + return; + } + + // Optional: Add a confirmation dialog here before proceeding + + setIsUploading(true); + setServerError(""); // Clear previous errors + + try { + // **IMPORTANT**: Ensure 'releaseProdSchedule' is implemented in your actions file + // to call the '/productionSchedule/detail/detailed/release' endpoint. + const response = await releaseProdSchedule({ + id: Number(scheduleId), + }) + + + if (response) { + router.refresh(); + } + } catch (e) { + console.error(e); + setServerError(t("An unexpected error occurred during global schedule release.")); + } finally { + setIsUploading(false); + } + }, [scheduleId, setIsUploading, t, router]); + // -------------------------------------------------------------------- + const [tempValue, setTempValue] = useState(null) const onEditClick = useCallback((rowId: number) => { const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) if (row) { setTempValue(row.demandQty) } - }, []) + }, [formProps]) const handleEditChange = useCallback((rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => { const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(newValue)) - }, []) + }, [formProps]) const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => { setIsUploading(true) @@ -175,6 +218,7 @@ const DetailedScheduleDetailView: React.FC = ({ if (response) { const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) + // Update BOM materials for the line after saving demand quantity formProps.setValue(`prodScheduleLines.${index}.bomMaterials`, response.entity.bomMaterials) } setIsUploading(false) @@ -182,14 +226,15 @@ const DetailedScheduleDetailView: React.FC = ({ console.log(e) setIsUploading(false) } - }, []) + }, [formProps, setIsUploading]) const onCancelClick = useCallback(async (rowId: number) => { - // if (tempValue) { + // Revert the demandQty to the temporary value stored on EditClick + if (tempValue !== null) { const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue)) - // } - }, [tempValue]) + } + }, [formProps, tempValue]) return ( <> @@ -200,9 +245,9 @@ const DetailedScheduleDetailView: React.FC = ({ onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > {/**/} - {/* */} - {/* {t(`${mode} ${title}`)}*/} - {/* */} + {/*  */} + {/*    {t(`${mode} ${title}`)}*/} + {/*  */} {/**/} = ({ isEditing={false} /> {/* - - */} + direction="row" + justifyContent="space-between" + flexWrap="wrap" + rowGap={2} + > + + */} {/* - - - */} + + + */} {serverError && ( {serverError} @@ -247,12 +289,24 @@ const DetailedScheduleDetailView: React.FC = ({ type={type} /> {/* {tabIndex === 1 && } */} + + {/* --- NEW BUTTON: Release Entire Schedule --- */} + + {/* ------------------------------------------- */} + {/* */} @@ -269,4 +323,4 @@ const DetailedScheduleDetailView: React.FC = ({ ); }; -export default DetailedScheduleDetailView; +export default DetailedScheduleDetailView; \ No newline at end of file diff --git a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx index 349d90b..9f3c6e1 100644 --- a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx +++ b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx @@ -17,28 +17,34 @@ const DetailedScheduleDetailWrapper: React.FC & SubComponents = async ({ id, type, }) => { - // const defaultValues = { - // id: 1, - // productionDate: "2025-05-07", - // totalJobOrders: 13, - // totalProductionQty: 21000, - // }; - - const prodSchedule = id ? await fetchDetailedProdScheduleDetail(id) : undefined - - if (prodSchedule) { - prodSchedule.prodScheduleLines = prodSchedule.prodScheduleLines.sort((a, b) => b.priority - a.priority) + const prodSchedule = id ? await fetchDetailedProdScheduleDetail(id) : undefined; + console.log("RAW API DATA:", prodSchedule?.prodScheduleLines[0]); // Check the actual keys here + + if (prodSchedule && prodSchedule.prodScheduleLines) { + // 1. Map the lines to ensure the new fields are explicitly handled + prodSchedule.prodScheduleLines = prodSchedule.prodScheduleLines.map(line => ({ + ...line, + // If the API uses different names (e.g., 'stockQty'), map them here: + // avgQtyLastMonth: line.avgQtyLastMonth ?? 0, + + // Ensure these keys match the 'field' property in your ViewByFGDetails.tsx columns + avgQtyLastMonth: line.avgQtyLastMonth || 0, + stockQty: line.stockQty || 0, + daysLeft: line.daysLeft || 0, + needNoOfJobOrder: line.needNoOfJobOrder || 0, + outputQty: line.outputQty || 0, + })).sort((a, b) => b.priority - a.priority); } + return ( ); }; DetailedScheduleDetailWrapper.Loading = GeneralLoading; -export default DetailedScheduleDetailWrapper; +export default DetailedScheduleDetailWrapper; \ No newline at end of file diff --git a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx index 3e43eee..151da2b 100644 --- a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx @@ -30,16 +30,16 @@ type Props = { onCancelClick: (rowId: number) => void; }; -// export type FGRecord = { -// id: string | number; -// code: string; -// name: string; -// inStockQty: number; -// productionQty?: number; -// purchaseQty?: number; -// }; - -const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => { +const ViewByFGDetails: React.FC = ({ + apiRef, + isEdit, + type, + onReleaseClick, + onEditClick, + handleEditChange, + onSaveClick, + onCancelClick +}) => { const { t, i18n: { language }, @@ -47,83 +47,20 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick const { getValues, - watch, formState: { errors, defaultValues, touchedFields }, } = useFormContext(); - // const apiRef = useGridApiRef(); - - // const [pagingController, setPagingController] = useState([ - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // { - // pageNum: 1, - // pageSize: 10, - // totalCount: 0, - // }, - // ]); - - // const updatePagingController = (updatedObj) => { - // setPagingController((prevState) => { - // return prevState.map((item, index) => { - // if (index === updatedObj?.index) { - // return { - // ...item, - // pageNum: item.pageNum, - // pageSize: item.pageSize, - // totalCount: item.totalCount, - // }; - // } else return item; - // }); - // }); - // }; - const columns = useMemo[]>( () => [ { field: "jobNo", label: t("Job No."), type: "read-only", - // editable: true, }, { field: "code", label: t("code"), type: "read-only", - // editable: true, }, { field: "name", @@ -134,109 +71,75 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick field: "type", label: t("type"), type: "read-only", - renderCell: (row) => { - return t(row.type); - }, - // editable: true, + renderCell: (row) => <>{t(row.type)}, }, - // { - // field: "inStockQty", - // label: "Available Qty", - // type: 'read-only', - // style: { - // textAlign: "right", - // }, - // // editable: true, - // renderCell: (row: FGRecord) => { - // if (typeof (row.inStockQty) == "number") { - // return decimalFormatter.format(row.inStockQty) - // } - // return row.inStockQty - // } - // }, { field: "demandQty", label: t("Demand Qty"), type: "input-number", - style: { - textAlign: "right", - // width: "100px", - }, - renderCell: (row) => { - if (typeof row.demandQty == "number") { - return integerFormatter.format(row.demandQty ?? 0); - } - return row.demandQty; - }, + style: { textAlign: "right" } as any, // Use 'as any' to bypass strict CSS validation + renderCell: (row) => <>{integerFormatter.format(row.demandQty ?? 0)}, }, { field: "uomName", label: t("UoM"), type: "read-only", - style: { - textAlign: "left", - // width: "100px", - }, - renderCell: (row) => { - return row.uomName; - }, + renderCell: (row) => <>{row.uomName}, + }, + // --- Added Avg Usage, Stock, Days Left, and Job Order Count --- + { + field: "avgQtyLastMonth", // This MUST match the key in the object + label: t("最近每日用量"), + type: "read-only", + // Ensure 'row' has the property 'avgQtyLastMonth' + renderCell: (row) => <>{decimalFormatter.format(row.avgQtyLastMonth ?? 0)}, + }, + { + field: "stockQty", + label: t("存貨量"), + type: "read-only", + style: { textAlign: "right" } as any, + renderCell: (row) => <>{decimalFormatter.format(row.stockQty ?? 0)}, }, { - field: "prodTimeInMinute", - label: t("Estimated Production Time"), + field: "daysLeft", + label: t("可用日"), type: "read-only", - style: { - textAlign: "right", - // width: "100px", - }, - renderCell: (row) => { - return - } + style: { textAlign: "right" } as any, + renderCell: (row) => <>{row.daysLeft ?? 0}, }, + { + field: "outputQty", + label: t("每批次生產數"), + type: "read-only", + style: { textAlign: "right", fontWeight: "bold" } as any, + renderCell: (row) => <>{row.outputQty ?? 0}, + }, + { + field: "needNoOfJobOrder", + label: t("生產批次"), + type: "read-only", + style: { textAlign: "right", fontWeight: "bold" } as any, + renderCell: (row) => <>{row.needNoOfJobOrder ?? 0}, + }, + // ------------------------------------------------------------- { field: "priority", label: t("Production Priority"), type: "read-only", - style: { - textAlign: "right", - // width: "100px", - }, - // editable: true, + style: { textAlign: "right" } as any, }, ], - [], + [t] ); return ( - {/* - - {t("FG Demand List (7 Days)")} - - - index={7} - items={fakeOverallRecords} - columns={overallColumns} - setPagingController={updatePagingController} - pagingController={pagingController[7]} - isAutoPaging={false} - isEditable={false} - isEdit={isEdit} - hasCollapse={true} - /> - */} - {/* {dayPeriod.map((date, index) => ( */} - {/* - {`${t("FG Demand Date")}: ${date}`} - */} type={type} - // items={fakeRecords[index]} // Use the corresponding records for the day - items={getValues("prodScheduleLines")} // Use the corresponding records for the day + items={getValues("prodScheduleLines")} columns={columns} - // setPagingController={updatePagingController} - // pagingController={pagingController[index]} isAutoPaging={false} isEditable={true} isEdit={isEdit} @@ -248,8 +151,8 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick onCancelClick={onCancelClick} /> - {/* ))} */} ); -}; -export default ViewByFGDetails; +}; // Added missing closing brace + +export default ViewByFGDetails; \ No newline at end of file diff --git a/src/components/JoSearch/JoCreateFormModal.tsx b/src/components/JoSearch/JoCreateFormModal.tsx index 62c508c..f8d8103 100644 --- a/src/components/JoSearch/JoCreateFormModal.tsx +++ b/src/components/JoSearch/JoCreateFormModal.tsx @@ -8,14 +8,16 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; import { isFinite } from "lodash"; -import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react"; +import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react"; import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { msg } from "../Swal/CustomAlerts"; +import { JobTypeResponse } from "@/app/api/jo/actions"; interface Props { open: boolean; bomCombo: BomCombo[]; + jobTypes: JobTypeResponse[]; onClose: () => void; onSearch: () => void; } @@ -23,6 +25,7 @@ interface Props { const JoCreateFormModal: React.FC = ({ open, bomCombo, + jobTypes, onClose, onSearch, }) => { @@ -30,19 +33,130 @@ const JoCreateFormModal: React.FC = ({ const formProps = useForm({ mode: "onChange", }); - const { reset, trigger, watch, control, register, formState: { errors } } = formProps + const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps + + // 监听 bomId 变化 + const selectedBomId = watch("bomId"); const onModalClose = useCallback(() => { reset() onClose() - }, []) + }, [reset, onClose]) + + const handleAutoCompleteChange = useCallback( + (event: SyntheticEvent, value: BomCombo, onChange: (...event: any[]) => void) => { + console.log("BOM changed to:", value); + onChange(value.id); + + // 1) 根据 BOM 设置数量 + if (value.outputQty != null) { + formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }); + } - const handleAutoCompleteChange = useCallback((event: SyntheticEvent, value: BomCombo, onChange: (...event: any[]) => void) => { - onChange(value.id) - if (value.outputQty != null) { - formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }) + // 2) 选 BOM 时,把日期默认设为“今天” + const today = dayjs(); + const todayStr = dayjsToDateString(today, "input"); // 你已经有的工具函数 + formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true }); + }, + [formProps] + ); + + // 使用 useMemo 来计算过滤后的 jobTypes,响应 selectedBomId 变化 + /* + const filteredJobTypes = useMemo(() => { + console.log("getFilteredJobTypes called, selectedBomId:", selectedBomId); + + if (!selectedBomId) { + console.log("No BOM selected, returning all jobTypes:", jobTypes); + return jobTypes; } - }, []) + + const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); + console.log("Selected BOM:", selectedBom); + console.log("Selected BOM full object:", JSON.stringify(selectedBom, null, 2)); + + if (!selectedBom) { + console.log("BOM not found, returning all jobTypes"); + return jobTypes; + } + + // 检查 description 是否存在 + const description = selectedBom.description; + console.log("BOM description (raw):", description); + console.log("BOM description type:", typeof description); + console.log("BOM description is undefined?", description === undefined); + console.log("BOM description is null?", description === null); + + if (!description) { + console.log("BOM description is missing or empty, returning all jobTypes"); + return jobTypes; + } + + const descriptionUpper = description.toUpperCase(); + console.log("BOM description (uppercase):", descriptionUpper); + console.log("All jobTypes:", jobTypes); + + let filtered: JobTypeResponse[] = []; + + if (descriptionUpper === "WIP") { + filtered = jobTypes.filter(jt => { + const jobTypeName = jt.name.toUpperCase(); + const shouldInclude = jobTypeName !== "FG"; + console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); + return shouldInclude; + }); + } else if (descriptionUpper === "FG") { + filtered = jobTypes.filter(jt => { + const jobTypeName = jt.name.toUpperCase(); + const shouldInclude = jobTypeName !== "WIP"; + console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); + return shouldInclude; + }); + } else { + filtered = jobTypes; + } + + console.log("Filtered jobTypes:", filtered); + return filtered; + }, [bomCombo, jobTypes, selectedBomId]); +*/ + // 当 BOM 改变时,自动选择匹配的 Job Type + useEffect(() => { + if (!selectedBomId) { + return; + } + + const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); + if (!selectedBom) { + return; + } + + const description = selectedBom.description; + console.log("Auto-select effect - BOM description:", description); + + if (!description) { + console.log("Auto-select effect - No description found, skipping auto-select"); + return; + } + + const descriptionUpper = description.toUpperCase(); + console.log("Auto-selecting Job Type for BOM description:", descriptionUpper); + + // 查找匹配的 Job Type + const matchingJobType = jobTypes.find(jt => { + const jobTypeName = jt.name.toUpperCase(); + const matches = jobTypeName === descriptionUpper; + console.log(`Checking JobType ${jt.name} (${jobTypeName}) against ${descriptionUpper}: ${matches}`); + return matches; + }); + + if (matchingJobType) { + console.log("Found matching Job Type, setting jobTypeId to:", matchingJobType.id); + setValue("jobTypeId", matchingJobType.id, { shouldValidate: true, shouldDirty: true }); + } else { + console.log("No matching Job Type found for description:", descriptionUpper); + } + }, [selectedBomId, bomCombo, jobTypes, setValue]); const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { if (value != null) { @@ -98,7 +212,7 @@ const JoCreateFormModal: React.FC = ({ = ({ /> - ( - - {t("Job Type")} - { const value = event.target.value; + console.log("Job Type changed to:", value); field.onChange(value === "" ? undefined : Number(value)); }} - > + > {t("Please select")} - {t("FG")} - {t("WIP")} - {t("R&D")} - {t("STF")} - {t("Other")} - - {/*{error && {error.message}}*/} - - )} - /> - + {/* {filteredJobTypes.map((jobType) => (*/} + {jobTypes.map((jobType) => ( + + {t(jobType.name)} + + ))} + + + ); + }} + /> + = ({ defaultInputs, bomCombo, printerCombo, jobT { setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 diff --git a/src/components/Jodetail/JoPickOrderList.tsx b/src/components/Jodetail/JoPickOrderList.tsx index a14176f..907738e 100644 --- a/src/components/Jodetail/JoPickOrderList.tsx +++ b/src/components/Jodetail/JoPickOrderList.tsx @@ -47,7 +47,10 @@ const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ useEffect(() => { fetchPickOrders(); }, [fetchPickOrders]); - + const handleBackToList = useCallback(() => { + setSelectedPickOrderId(undefined); + setSelectedJobOrderId(undefined); + }, []); // If a pick order is selected, show JobPickExecution detail view if (selectedPickOrderId !== undefined) { return ( @@ -64,7 +67,11 @@ const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ {t("Back to List")} - + ); } diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index 15007b3..3cf7b27 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -196,16 +196,19 @@ useEffect(() => { if (verifiedQty === undefined || verifiedQty < 0) { newErrors.actualPickQty = t('Qty is required'); } - + const totalQty = verifiedQty + badItemQty + missQty; const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; - + + // ✅ 新增:必须至少有一个 > 0 + if (!hasAnyValue) { + newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0'); + } + if (hasAnyValue && totalQty !== requiredQty) { newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); } - - - + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -214,9 +217,10 @@ useEffect(() => { return; } - // Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0) - const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0)) - && formData.missQty == 0 && formData.badItemQty == 0; + // ✅ 只允许 Verified>0 且没有问题时,走 normal pick + const isNormalPick = verifiedQty > 0 + && formData.missQty == 0 + && formData.badItemQty == 0; if (isNormalPick) { if (onNormalPickSubmit) { @@ -235,11 +239,12 @@ useEffect(() => { } return; } - + + // ❌ 有问题(或全部为 0)才进入 Issue 提报流程 if (!validateForm() || !formData.pickOrderId) { return; } - + setLoading(true); try { const submissionData = { diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index d43a661..10471f1 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -487,7 +487,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { matchStatus: lot.matchStatus, routerArea: lot.routerArea, routerRoute: lot.routerRoute, - uomShortDesc: lot.uomShortDesc + uomShortDesc: lot.uomShortDesc, + handler: lot.handler, }); }); } @@ -574,7 +575,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => - lot.matchStatus === 'scanned' + lot.matchStatus === 'scanned'|| + lot.stockOutLineStatus === 'completed' ); if (scannedLots.length === 0) { @@ -614,7 +616,13 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { if (successCount > 0) { setQrScanSuccess(true); - setTimeout(() => setQrScanSuccess(false), 2000); + setTimeout(() => { + setQrScanSuccess(false); + // 添加:提交成功后返回到列表 + if (onBack) { + onBack(); + } + }, 2000); } } catch (error: any) { @@ -634,7 +642,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { } finally { setIsSubmittingAll(false); } - }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign]); + }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign, onBack]); const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; @@ -1112,7 +1120,25 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { )} {/* Combined Lot Table */} + + + {/* {!isManualScanning ? ( @@ -1166,18 +1192,19 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { {t("QR code verified.")} )} - + */} {t("Index")} {t("Route")} + {t("Handler")} {t("Item Code")} {t("Item Name")} {t("Lot No")} {t("Lot Required Pick Qty")} - {t("Scan Result")} + {/* {t("Scan Result")} */} {t("Submit Required Pick Qty")} @@ -1212,6 +1239,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { {lot.routerRoute || '-'} + {lot.handler || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} @@ -1232,7 +1260,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} - + {/* {lot.matchStatus?.toLowerCase() === 'scanned' || lot.matchStatus?.toLowerCase() === 'completed' ? ( @@ -1266,7 +1294,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { )} - + */} @@ -1277,9 +1305,10 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; handlePickQtyChange(lotKey, submitQty); handleSubmitPickQtyWithQty(lot, submitQty); + updateSecondQrScanStatus(lot.pickOrderLineId, lot.lotId, currentUserId || 0, submitQty); }} disabled={ - lot.matchStatus !== 'scanned' || + //lot.matchStatus !== 'scanned' || lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable' || lot.lotAvailability === 'rejected' @@ -1291,7 +1320,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { minWidth: '70px' }} > - {t("Submit")} + {t("Confirm")} - ))} - - -)} - -*/} + {/* Header section with printer selection */} + + {/* Left side - Title */} - - - + {/* Right side - Printer selection (only show on tab 1) */} + {tabIndex === 1 && ( + + + {t("Select Printer")}: + + + option.name || option.label || option.code || `Printer ${option.id}` + } + value={selectedPrinter} + onChange={(_, newValue) => setSelectedPrinter(newValue)} + sx={{ minWidth: 200 }} + size="small" + renderInput={(params) => ( + + )} + /> + + {t("Print Quantity")}: + + { + const value = parseInt(e.target.value) || 1; + setPrintQty(Math.max(1, value)); + }} + inputProps={{ min: 1, step: 1 }} + sx={{ width: 120 }} + size="small" + /> + + )} + - {/* Tabs section - Move the click handler here */} + {/* Tabs section */} - {/* */} - - - - {/* */} - {/* */} + + - - {/* Content section - NO overflow: 'auto' here */} - - {/* {tabIndex === 0 && } */} - {tabIndex === 1 && } + {/* Content section */} + {tabIndex === 0 && } - {/* {tabIndex === 2 && } */} - {/* {tabIndex === 3 && } */} + {tabIndex === 1 && ( + + )} ); diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index f54d545..53f0f0b 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; interface Props { filterArgs: Record; printerCombo: PrinterCombo[]; + selectedPrinter?: PrinterCombo | null; + printQty?: number; } // 修改:已完成的 Job Order Pick Order 接口 @@ -99,9 +101,15 @@ interface LotDetail { itemName: string; uomCode: string; uomDesc: string; + match_status: string; } -const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => { +const CompleteJobOrderRecord: React.FC = ({ + filterArgs, + printerCombo, + selectedPrinter: selectedPrinterProp, + printQty: printQtyProp +}) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -121,25 +129,11 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => // 修改:搜索状态 const [searchQuery, setSearchQuery] = useState>({}); const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState([]); - //const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); - const defaultDemoPrinter: PrinterCombo = { - id: 2, - value: 2, - name: "2fi", - label: "2fi", - code: "2fi" - }; - const availablePrinters = useMemo(() => { - if (printerCombo.length === 0) { - console.log("No printers available, using default demo printer"); - return [defaultDemoPrinter]; - } - return printerCombo; - }, [printerCombo]); - const [selectedPrinter, setSelectedPrinter] = useState( - printerCombo && printerCombo.length > 0 ? printerCombo[0] : null - ); - const [printQty, setPrintQty] = useState(1); + + // Use props with fallback + const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); + const printQty = printQtyProp ?? 1; + // 修改:分页状态 const [paginationController, setPaginationController] = useState({ pageNum: 0, @@ -157,7 +151,7 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => try { console.log("🔍 Fetching completed Job Order pick orders (pick completed only)..."); - const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId); + const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(); // Fix: Ensure the data is always an array const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; @@ -226,7 +220,19 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => setFilteredJobOrderPickOrders(filtered); console.log("Filtered Job Order pick orders count:", filtered.length); }, [completedJobOrderPickOrders]); - + const formatDateTime = (value: any) => { + if (!value) return "-"; + + // 后端发来的是 [yyyy, MM, dd, HH, mm, ss] + if (Array.isArray(value)) { + const [year, month, day, hour = 0, minute = 0, second = 0] = value; + return new Date(year, month - 1, day, hour, minute, second).toLocaleString(); + } + + // 如果以后改成字符串/ISO,也兼容 + const d = new Date(value); + return isNaN(d.getTime()) ? "-" : d.toLocaleString(); + }; // 修改:重置搜索 const handleSearchReset = useCallback(() => { setSearchQuery({}); @@ -433,18 +439,6 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => {t("Required Qty")}: {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} - {/* - - - - */} @@ -545,12 +539,12 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => height: '100%' }}> = ({ filterArgs ,printerCombo}) => {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} - - - - {t("Select Printer")}: - - option.name || option.label || option.code || `Printer ${option.id}`} - value={selectedPrinter} - onChange={(_, newValue) => setSelectedPrinter(newValue)} - sx={{ minWidth: 250 }} - size="small" - renderInput={(params) => } - /> - - {t("Print Quantity")}: - - { - const value = parseInt(e.target.value) || 1; - setPrintQty(Math.max(1, value)); - }} - inputProps={{ min: 1, step: 1 }} - sx={{ width: 120 }} - size="small" - /> - - + {/* 列表 */} {filteredJobOrderPickOrders.length === 0 ? ( @@ -652,7 +616,7 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} - {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} + {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 2ba0e66..9e0590d 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -42,8 +42,6 @@ import { } from "@/app/api/pickOrder/actions"; // 修改:使用 Job Order API import { - //fetchJobOrderLotsHierarchical, - //fetchUnassignedJobOrderPickOrders, assignJobOrderPickOrder, fetchJobOrderLotsHierarchicalByPickOrderId, updateJoPickOrderHandledBy, @@ -67,7 +65,8 @@ import FGPickOrderCard from "./FGPickOrderCard"; import LotConfirmationModal from "./LotConfirmationModal"; interface Props { filterArgs: Record; - onSwitchToRecordTab: () => void; + //onSwitchToRecordTab: () => void; + onBackToList?: () => void; } // QR Code Modal Component (from GoodPickExecution) @@ -324,7 +323,7 @@ const QrCodeModal: React.FC<{ ); }; -const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) => { +const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -412,6 +411,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) pickOrderType: data.pickOrder.type, pickOrderStatus: data.pickOrder.status, pickOrderAssignTo: data.pickOrder.assignTo, + handler: line.handler, }); }); } @@ -537,6 +537,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setCombinedDataLoading(false); } }, [getAllLotsFromHierarchical]); + const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { if (!currentUserId || !pickOrderId || !itemId) { return; @@ -901,11 +902,9 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) // Use the first active suggested lot as the "expected" lot const expectedLot = activeSuggestedLots[0]; - // 2) Check if the scanned lot matches exactly if (scanned?.lotNo === expectedLot.lotNo) { - // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); - + if (!expectedLot.stockOutLineId) { console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); setQrScanError(true); @@ -922,24 +921,33 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) status: "checked", }); - if (res.code === "checked" || res.code === "SUCCESS") { - setQrScanError(false); - setQrScanSuccess(true); + const updateOk = + res?.type === "checked" || + typeof res?.id === "number" || + (res?.message && res.message.includes("success")); + + if (updateOk) { + setQrScanError(false); + setQrScanSuccess(true); - // ✅ 刷新数据而不是直接更新 state - const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; - await fetchJobOrderData(pickOrderId); - console.log("✅ Status updated, data refreshed"); - } else if (res.code === "LOT_NUMBER_MISMATCH") { - console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); - setQrScanError(true); - setQrScanSuccess(false); - } else if (res.code === "ITEM_MISMATCH") { - console.warn("Backend reported ITEM_MISMATCH:", res.message); + + if ( + expectedLot.pickOrderId && + expectedLot.itemId && + (expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" || + !expectedLot.stockOutLineStatus) && + !expectedLot.handler + ) { + await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId); + } + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + } else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") { setQrScanError(true); setQrScanSuccess(false); } else { - console.warn("Unexpected response code from backend:", res.code); + console.warn("Unexpected response from backend:", res); setQrScanError(true); setQrScanSuccess(false); } @@ -949,7 +957,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setQrScanSuccess(false); } - return; // ✅ 直接返回,不再调用 handleQrCodeSubmit + return; // ✅ 直接返回,不再调用后面的分支 } // Case 2: Same item, different lot - show confirmation modal @@ -977,7 +985,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setQrScanSuccess(false); return; } - }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); + }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); const handleManualInputSubmit = useCallback(() => { @@ -1310,6 +1318,14 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) console.error("Error submitting pick quantity:", error); } }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); + const handleSkip = useCallback(async (lot: any) => { + try { + console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo); + await handleSubmitPickQtyWithQty(lot, 0); + } catch (err) { + console.error("Error in Skip:", err); + } + }, [handleSubmitPickQtyWithQty]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked' @@ -1365,8 +1381,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setTimeout(() => { setQrScanSuccess(false); checkAndAutoAssignNext(); - if (onSwitchToRecordTab) { - onSwitchToRecordTab(); + if (onBackToList) { + onBackToList(); } }, 2000); } else { @@ -1380,7 +1396,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) } finally { setIsSubmittingAll(false); } - }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab]) + }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList]) // Calculate scanned items count const scannedItemsCount = useMemo(() => { @@ -1544,7 +1560,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) }, [startScan]); const handleStopScan = useCallback(() => { - console.log("⏹️ Stopping manual QR scan..."); + console.log(" Stopping manual QR scan..."); setIsManualScanning(false); setQrScanError(false); setQrScanSuccess(false); @@ -1563,7 +1579,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) }, [isManualScanning, stopScan, resetScan]); useEffect(() => { if (isManualScanning && combinedLotData.length === 0) { - console.log("⏹️ No data available, auto-stopping QR scan..."); + console.log(" No data available, auto-stopping QR scan..."); handleStopScan(); } }, [combinedLotData.length, isManualScanning, handleStopScan]); @@ -1677,16 +1693,59 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) - {qrScanError && !qrScanSuccess && ( - + {qrScanError && !qrScanSuccess && ( + {t("QR code does not match any item in current orders.")} )} - {qrScanSuccess && ( - - {t("QR code verified.")} - - )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )}
@@ -1694,6 +1753,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) {t("Index")} {t("Route")} + {t("Handler")} {t("Item Code")} {t("Item Name")} {t("Lot No")} @@ -1733,6 +1793,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) {lot.routerRoute || '-'} + {lot.handler || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} @@ -1837,6 +1898,15 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) > {t("Issue")} + diff --git a/src/components/ProductionProcess/FinishedQcJobOrderList.tsx b/src/components/ProductionProcess/FinishedQcJobOrderList.tsx new file mode 100644 index 0000000..1d05b5c --- /dev/null +++ b/src/components/ProductionProcess/FinishedQcJobOrderList.tsx @@ -0,0 +1,255 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import { + Box, + Button, + Stack, + Typography, + Chip, + CircularProgress, + TablePagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Tooltip, + FormControl, + InputLabel, + Select, + MenuItem, +} from "@mui/material"; +import QrCodeIcon from '@mui/icons-material/QrCode'; +import { useTranslation } from "react-i18next"; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { + fetchJoForPrintQrCode, + JobOrderListForPrintQrCodeResponse, + printFGStockInLabel, + PrintFGStockInLabelRequest, +} from "@/app/api/jo/actions"; +import { PrinterCombo } from "@/app/api/settings/printer"; + +interface FinishedQcJobOrderListProps { + printerCombo: PrinterCombo[]; + selectedPrinter: PrinterCombo | null; +} + +const PER_PAGE = 10; + +const FinishedQcJobOrderList: React.FC = ({ + printerCombo, + selectedPrinter, +}) => { + const { t } = useTranslation(["common"]); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + const [loading, setLoading] = useState(false); + const [jobOrders, setJobOrders] = useState([]); + const [page, setPage] = useState(0); + const [isPrinting, setIsPrinting] = useState(false); + const [printingId, setPrintingId] = useState(null); + const [selectedDate, setSelectedDate] = useState("today"); + const getDateLabel = (offset: number) => { + return dayjs().subtract(offset, 'day').format('YYYY-MM-DD'); + }; + + // 根据选择的日期获取实际日期字符串 + const getDateParam = (dateOption: string): string => { + if (dateOption === "today") { + return dayjs().format('YYYY-MM-DD'); + } else if (dateOption === "yesterday") { + return dayjs().subtract(1, 'day').format('YYYY-MM-DD'); + } else if (dateOption === "dayBeforeYesterday") { + return dayjs().subtract(2, 'day').format('YYYY-MM-DD'); + } + return dayjs().format('YYYY-MM-DD'); + }; + + const fetchJobOrders = useCallback(async () => { + setLoading(true); + try { + const dateParam = getDateParam(selectedDate); + const data = await fetchJoForPrintQrCode(dateParam); + setJobOrders(data || []); + setPage(0); + } catch (e) { + console.error(e); + setJobOrders([]); + } finally { + setLoading(false); + } + }, [selectedDate]); + + + + useEffect(() => { + fetchJobOrders(); + }, [fetchJobOrders]); + + const handlePrint = useCallback(async (jobOrder: JobOrderListForPrintQrCodeResponse) => { + if (!selectedPrinter) { + alert(t("Please select a printer")); + return; + } + + // Use stockInLineId from the response (assuming backend returns it) + // If the backend still returns stockOutLineId, you may need to update the interface + const stockInLineId = (jobOrder as any).stockInLineId || jobOrder.stockOutLineId; + + if (!stockInLineId) { + alert(t("Invalid Stock In Line Id")); + return; + } + + try { + setIsPrinting(true); + setPrintingId(jobOrder.id); + + const data: PrintFGStockInLabelRequest = { + stockInLineId: stockInLineId, + printerId: selectedPrinter.id, + printQty: 1 // Default to 1 + }; + + const response = await printFGStockInLabel(data); + + if (response) { + console.log("Print response:", response); + alert(t("Print job sent successfully")); + } + } catch (error: any) { + console.error("Error printing:", error); + alert(t(`Print failed: ${error?.message || "Unknown error"}`)); + } finally { + setIsPrinting(false); + setPrintingId(null); + } + }, [selectedPrinter, t]); + + const startIdx = page * PER_PAGE; + const paged = jobOrders.slice(startIdx, startIdx + PER_PAGE); + + return ( + + {/* Date Selector */} + + + + {t("Select Date")} + + + + + + {loading ? ( + + + + ) : ( + + + {t("Total finished QC job orders")}: {jobOrders.length} + + + +
+ + + {t("Code")} + {t("Name")} + {t("Required Qty")} + {t("Finished Time")} + {t("Action")} + + + + {paged.map((jobOrder) => { + const statusColor = jobOrder.stockOutLineStatus === "completed" + ? "success" + : "default"; + + const finishedTimeDisplay = jobOrder.finihedTime + ? dayjs(jobOrder.finihedTime).format(OUTPUT_DATE_FORMAT) + : "-"; + + const isCurrentlyPrinting = isPrinting && printingId === jobOrder.id; + + return ( + + + {jobOrder.code} + + {jobOrder.name} + {jobOrder.reqQty} + + {finishedTimeDisplay} + + + handlePrint(jobOrder)} + disabled={isPrinting || printerCombo.length <= 0 || !selectedPrinter} + size="small" + > + {isCurrentlyPrinting ? ( + + ) : ( + + )} + + + + + ); + })} + +
+
+ + {jobOrders.length > 0 && ( + setPage(p)} + rowsPerPageOptions={[PER_PAGE]} + /> + )} +
+ )} +
+ ); +}; + +export default FinishedQcJobOrderList; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 6723fc4..e972450 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -33,16 +33,11 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { - fetchProductProcessById, - updateProductProcessLineQrscan, + // updateProductProcessLineQrscan, + newUpdateProductProcessLineQrscan, fetchProductProcessLineDetail, - ProductProcessLineDetailResponse, JobOrderProcessLineDetailResponse, - updateLineOutput, ProductProcessLineInfoResponse, - ProductProcessResponse, - ProductProcessLineResponse, - completeProductProcessLine, startProductProcessLine, fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions"; @@ -61,7 +56,7 @@ const ProductionProcessDetail: React.FC = ({ onBack, fromJosave, }) => { - const { t } = useTranslation(); + const { t } = useTranslation("common"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -80,8 +75,10 @@ const ProductionProcessDetail: React.FC = ({ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedOperatorId, setScannedOperatorId] = useState(null); const [scannedEquipmentId, setScannedEquipmentId] = useState(null); - const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState(null); + // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState(null); const [scannedStaffNo, setScannedStaffNo] = useState(null); + // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState(null); + const [scannedEquipmentCode, setScannedEquipmentCode] = useState(null); const [scanningLineId, setScanningLineId] = useState(null); const [lineDetailForScan, setLineDetailForScan] = useState(null); const [showScanDialog, setShowScanDialog] = useState(false); @@ -224,7 +221,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { const currentLine = lines.find(l => l.id === lineId); if (currentLine && currentLine.equipment_name) { const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`; - setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); + setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`); } else { // 如果找不到 line,尝试从 API 获取 line detail @@ -232,11 +229,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { fetchProductProcessLineDetail(lineId) .then((lineDetail) => { // 从 lineDetail 中获取 equipment_name - // 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整 const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; if (equipmentName) { const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; - setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); + setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`); } else { console.warn(`Equipment name not found in line detail for lineId: ${lineId}`); @@ -249,7 +245,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { return; } - // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo // 例如:{2fitestu123} = staffNo: "123" // 例如:{2fitestustaff001} = staffNo: "staff001" @@ -271,11 +266,11 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { return; } - // 检查 equipmentTypeSubTypeEquipmentNo 格式 - const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo):\s*(.+)$/i); + // 检查 equipmentCode 格式 + const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i); if (equipmentCodeMatch) { const equipmentCode = equipmentCodeMatch[1].trim(); - setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode); + setScannedEquipmentCode(equipmentCode); return; } @@ -286,11 +281,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setScannedStaffNo(String(qrData.staffNo)); } if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) { - setScannedEquipmentTypeSubTypeEquipmentNo( + setScannedEquipmentCode( String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode) ); } - // TODO: 处理 JSON 格式的 QR 码 } catch { // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode if (trimmedValue.length > 0) { @@ -299,7 +293,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setScannedStaffNo(trimmedValue); } else if (trimmedValue.includes("-")) { // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") - setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue); + setScannedEquipmentCode(trimmedValue); } } } @@ -323,36 +317,41 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { console.log("submitScanAndStart called with:", { lineId, scannedStaffNo, - scannedEquipmentTypeSubTypeEquipmentNo, + // scannedEquipmentTypeSubTypeEquipmentNo, + scannedEquipmentCode, }); if (!scannedStaffNo) { console.log("No staffNo, cannot submit"); setIsAutoSubmitting(false); - return false; // 没有 staffNo,不能提交 + return false; } try { - // 获取 line detail 以检查 bomProcessEquipmentId const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); - - // 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo - console.log("Submitting scan data:", { + + // ✅ 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo) + const effectiveEquipmentCode = + scannedEquipmentCode ?? null; + + + console.log("Submitting scan data with equipmentCode:", { productProcessLineId: lineId, staffNo: scannedStaffNo, - equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo, + equipmentCode: effectiveEquipmentCode, }); - const response = await updateProductProcessLineQrscan({ + const response = await newUpdateProductProcessLineQrscan({ productProcessLineId: lineId, - equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined, - staffNo: scannedStaffNo || undefined, + equipmentCode: effectiveEquipmentCode ?? "", + staffNo: scannedStaffNo, }); console.log("Scan submit response:", response); - // 检查响应中的 message 字段来判断是否成功 - if (response && response.message) { + if (response && response.type === "error") { + console.error("Scan validation failed:", response.message); + alert(t(response.message) || t("Validation failed. Please check your input.")); setIsAutoSubmitting(false); if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); @@ -360,25 +359,31 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { } return false; } - - // 验证通过,继续执行后续步骤 + console.log("Validation passed, starting line..."); handleStopScan(); setShowScanDialog(false); setIsAutoSubmitting(false); - + await handleStartLine(lineId); setSelectedLineId(lineId); setIsExecutingLine(true); await fetchProcessDetail(); - + return true; } catch (error) { console.error("Error submitting scan:", error); + alert("Failed to submit scan data. Please try again."); setIsAutoSubmitting(false); return false; } - }, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]); + }, [ + scannedStaffNo, + scannedEquipmentCode, + lineDetailForScan, + t, + fetchProcessDetail, + ]); const handleSubmitScanAndStart = useCallback(async (lineId: number) => { console.log("handleSubmitScanAndStart called with lineId:", lineId); @@ -408,6 +413,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setProcessedQrCodes(new Set()); setScannedOperatorId(null); setScannedEquipmentId(null); + setScannedStaffNo(null); // ✅ Add this + setScannedEquipmentCode(null); setIsAutoSubmitting(false); // 添加:重置自动提交状态 setLineDetailForScan(null); // 获取 line detail 以获取 bomProcessEquipmentId @@ -431,7 +438,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { } setIsManualScanning(false); - setIsAutoSubmitting(false); // 添加:重置自动提交状态 + setIsAutoSubmitting(false); + setScannedStaffNo(null); // ✅ Add this + setScannedEquipmentCode(null); stopScan(); resetScan(); }, [stopScan, resetScan]); @@ -446,20 +455,21 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { } }; // 提交扫描结果并验证 - + /* useEffect(() => { console.log("Auto-submit check:", { scanningLineId, scannedStaffNo, - scannedEquipmentTypeSubTypeEquipmentNo, + scannedEquipmentCode, isAutoSubmitting, isManualScanning, }); + // ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId if ( scanningLineId && scannedStaffNo !== null && - scannedEquipmentTypeSubTypeEquipmentNo !== null && + (scannedEquipmentCode !== null) && !isAutoSubmitting && isManualScanning ) { @@ -484,7 +494,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { // 注意:这里不立即清除定时器,因为我们需要它执行 // 只在组件卸载时清除 }; - }, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]); + }, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]); + */ useEffect(() => { return () => { if (autoSubmitTimerRef.current) { @@ -502,6 +513,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setScannedEquipmentId(null); setProcessedQrCodes(new Set()); + setScannedStaffNo(null); + setScannedEquipmentCode(null); + setProcessedQrCodes(new Set()); // 清除之前的定时器 if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); @@ -764,9 +778,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { - {scannedEquipmentTypeSubTypeEquipmentNo - ? `${t("Equipment Type/Code")}: ${scannedEquipmentTypeSubTypeEquipmentNo}` - : t("Please scan equipment code (optional if not required)") + {/* ✅ Show both options */} + {scannedEquipmentCode + ? `${t("Equipment Code")}: ${scannedEquipmentCode}` + : t("Please scan equipment code") } @@ -792,7 +807,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 166382c..1e1ca0b 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -115,7 +115,7 @@ const ProductionProcessJobOrderDetail: React.FC { - if (line.type?.toLowerCase() === "consumables") { + if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { return null; } const inventory = inventoryData.find(inv => @@ -158,7 +158,7 @@ const isStockSufficient = (line: JobOrderLine) => { const stockCounts = useMemo(() => { // 过滤掉 consumables 类型的 lines const nonConsumablesLines = jobOrderLines.filter( - line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" + line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm" ); const total = nonConsumablesLines.length; const sufficient = nonConsumablesLines.filter(isStockSufficient).length; @@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { const response = await deleteJobOrder(jobOrderId) if (response) { //setProcessData(response.entity); - await fetchData(); + //await fetchData(); + onBack(); } }, [jobOrderId]); const handleRelease = useCallback(async ( jobOrderId: number) => { @@ -315,7 +316,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { headerAlign: "left", type: "number", renderCell: (params) => { - return {params.value}; + return {params.value}; }, }, { @@ -325,16 +326,28 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { align: "left", headerAlign: "left", renderCell: (params) => { - return {params.value || ""}; + return( + +   + {params.value || ""} + +   + + ) + }, }, + ]; const productionProcessesLineRemarkTableRows = processData?.productProcessLines?.map((line: any) => ({ id: line.seqNo, seqNo: line.seqNo, + description: line.description ?? "", + + })) ?? []; @@ -486,21 +499,37 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { />
); - const ProductionProcessesLineRemarkTableContent = () => ( - - - 'auto'} - - /> - - ); + const ProductionProcessesLineRemarkTableContent = () => ( + + + 'auto'} + hideFooter={false} // ✅ Ensure footer is visible + /> + +); return ( diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 8a46fc2..d52128d 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -94,7 +94,8 @@ const ProductProcessList: React.FC = ({ onSelectProcess setModalInfo({ id: process.stockInLineId, - expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), + //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), + // 视需要补 itemId、jobOrderId 等 }); setOpenModal(true); @@ -155,9 +156,10 @@ const ProductProcessList: React.FC = ({ onSelectProcess const closeNewModal = useCallback(() => { // const response = updateJo({ id: 1, status: "storing" }); setOpenModal(false); // Close the modal first + fetchProcesses(); // setTimeout(() => { // }, 300); // Add a delay to avoid immediate re-trigger of useEffect -}, []); +}, [fetchProcesses]); const startIdx = page * PER_PAGE; const paged = processes.slice(startIdx, startIdx + PER_PAGE); @@ -233,10 +235,10 @@ const ProductProcessList: React.FC = ({ onSelectProcess
- {t("Item Name")}: {process.itemName} + {t("Item Name")}: {process.itemCode} {process.itemName} - {t("Required Qty")}: {process.requiredQty} + {t("Required Qty")}: {process.requiredQty} {process.uom} {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} @@ -268,7 +270,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess @@ -462,6 +696,7 @@ const ProductionProcessStepExecution: React.FC setShowOutputTable(true)} > {t("Order Complete")} @@ -521,39 +756,7 @@ const ProductionProcessStepExecution: React.FC - {/* byproduct */} - {/* - - - - {t("By-product")} - - - - setOutputData({ - ...outputData, - byproductQty: parseInt(e.target.value) || 0 - })} - /> - - - setOutputData({ - ...outputData, - byproductUom: e.target.value - })} - /> - - - */} + {/* defect 1 */} diff --git a/src/components/Qc/QcComponent.tsx b/src/components/Qc/QcComponent.tsx index 089dfcc..031e93d 100644 --- a/src/components/Qc/QcComponent.tsx +++ b/src/components/Qc/QcComponent.tsx @@ -413,10 +413,11 @@ useEffect(() => { } else { return 60} }; - const formattedDesc = (content: string = "") => { + const formattedDesc = (content: string | null | undefined = "") => { + const safeContent = content || ""; return ( <> - {content.split("\\n").map((line, index) => ( + {safeContent.split("\\n").map((line, index) => ( {line}
))} diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index bf40183..035325b 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -40,7 +40,7 @@ import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForS import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; import FgStockInForm from "../StockIn/FgStockInForm"; import LoadingComponent from "../General/LoadingComponent"; -import { printFGStockInLabel, PrintFGStockInLabelRequest } from "@/app/api/jo/actions"; +import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions"; const style = { position: "absolute", @@ -119,7 +119,7 @@ const QcStockInModal: React.FC = ({ const res = await fetchStockInLineInfo(stockInLineId); if (res) { console.log("%c Fetched Stock In Line: ", "color:orange", res); - setStockInLineInfo({...inputDetail, ...res, expiryDate: inputDetail?.expiryDate}); // TODO review to overwrite res with inputDetail instead (revise PO fetching data) + setStockInLineInfo({...inputDetail, ...res, expiryDate: res.expiryDate}); // fetchQcResultData(stockInLineId); } else throw("Result is undefined"); @@ -168,8 +168,8 @@ const QcStockInModal: React.FC = ({ { ...d, // status: d.status ?? "pending", - productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : undefined, - expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined, + productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : dayjs().format(INPUT_DATE_FORMAT), + expiryDate: d.expiryDate ? (Array.isArray(d.expiryDate) ? arrayToDateString(d.expiryDate, "input") : d.expiryDate) : undefined, receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0, @@ -350,9 +350,9 @@ const QcStockInModal: React.FC = ({ const qcData = { dnNo : data.dnNo? data.dnNo : "DN00000", // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), - productionDate : arrayToDateString(data.productionDate, "input"), - expiryDate : arrayToDateString(data.expiryDate, "input"), - receiptDate : arrayToDateString(data.receiptDate, "input"), + productionDate : data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, + expiryDate : data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined, + receiptDate : data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, qcAccept: qcAccept? qcAccept : false, acceptQty: acceptQty? acceptQty : 0, @@ -396,6 +396,52 @@ const QcStockInModal: React.FC = ({ // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", // confirmButtonText: t("confirm putaway"), html: ""}); // onOpenPutaway(); + const isJobOrderBom = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess") + && stockInLineInfo?.bomDescription === "WIP"; + if (isJobOrderBom) { + // Auto putaway to default warehouse + const defaultWarehouseId = stockInLineInfo?.defaultWarehouseId ?? 1; + + // Get warehouse name from warehouse prop or use default + let defaultWarehouseName = "2F-W201-#A-01"; // Default warehouse name + if (warehouse && warehouse.length > 0) { + const defaultWarehouse = warehouse.find(w => w.id === defaultWarehouseId); + if (defaultWarehouse) { + defaultWarehouseName = `${defaultWarehouse.code} - ${defaultWarehouse.name}`; + } + } + + // Create putaway data + const putawayData = { + id: stockInLineInfo?.id, // Include ID + itemId: stockInLineInfo?.itemId, // Include Item ID + purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists + purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL ID if exists + acceptedQty:acceptQty, // Include acceptedQty + acceptQty: stockInLineInfo?.acceptedQty, // Putaway quantity + warehouseId: defaultWarehouseId, + status: "received", // Use string like PutAwayModal + productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, + expiryDate: data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined, + receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, + inventoryLotLines: [{ + warehouseId: defaultWarehouseId, + qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal + }], + } as StockInLineEntry & ModalFormInput; + + try { + // Use updateStockInLine directly like PutAwayModal does + const res = await updateStockInLine(putawayData); + if (Boolean(res.id)) { + console.log("Auto putaway completed for job order bom"); + } + } catch (error) { + console.error("Error during auto putaway:", error); + alert(t("Auto putaway failed. Please complete putaway manually.")); + } + + } closeHandler({}, "backdropClick"); // setTabIndex(1); // Need to go Putaway tab? } else { @@ -540,24 +586,38 @@ const QcStockInModal: React.FC = ({ // return isPassed // }, [acceptQty, formProps]) - const printQrcode = useCallback( - async () => { - setIsPrinting(true); - try { - const postData = { stockInLineIds: [stockInLineInfo?.id] }; - const response = await fetchPoQrcode(postData); - if (response) { - console.log(response); - downloadFile(new Uint8Array(response.blobValue), response.filename!); - } - } catch (e) { - console.log("%c Error downloading QR Code", "color:red", e); - } finally { +const printQrcode = useCallback( + async () => { + setIsPrinting(true); + try { + let response; + + if (printSource === "productionProcess") { + // Use FG Stock In Label download API for production process + if (!stockInLineInfo?.id) { + console.error("Stock In Line ID is required for download"); setIsPrinting(false); + return; } - }, - [stockInLineInfo], - ); + const postData = { stockInLineId: stockInLineInfo.id }; + response = await fetchFGStockInLabel(postData); + } else { + const postData = { stockInLineIds: [stockInLineInfo?.id] }; + response = await fetchPoQrcode(postData); + } + + if (response) { + console.log(response); + downloadFile(new Uint8Array(response.blobValue), response.filename!); + } + } catch (e) { + console.log("%c Error downloading QR Code", "color:red", e); + } finally { + setIsPrinting(false); + } + }, + [stockInLineInfo, printSource], +); return ( <> diff --git a/src/components/StockIn/FgStockInForm.tsx b/src/components/StockIn/FgStockInForm.tsx index af46970..bad913f 100644 --- a/src/components/StockIn/FgStockInForm.tsx +++ b/src/components/StockIn/FgStockInForm.tsx @@ -12,11 +12,12 @@ import { TextField, Tooltip, Typography, + Button, } from "@mui/material"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { GridColDef, GridRowIdGetter, @@ -35,6 +36,11 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import dayjs from "dayjs"; +import CalculateExpiryDateModal from "./CalculateExpiryDateModal"; +import { InputAdornment } from "@mui/material"; +import { dayjsToDateString } from "@/app/utils/formatUtil"; + + // change PurchaseQcResult to stock in entry props interface Props { itemDetail: StockInLine; @@ -115,6 +121,8 @@ const FgStockInForm: React.FC = ({ console.log(errors); }, [errors]); + const [openModal, setOpenModal] = useState(false); + const [openExpDatePicker, setOpenExpDatePicker] = useState(false); const productionDate = watch("productionDate"); const expiryDate = watch("expiryDate"); const uom = watch("uom"); @@ -140,7 +148,23 @@ const FgStockInForm: React.FC = ({ console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); }, [itemDetail]); - return ( + const handleOpenModal = useCallback(() => { + setOpenModal(true); + }, []); + + const handleOnModalClose = useCallback(() => { + setOpenExpDatePicker(false); + setOpenModal(false); + }, []); + + const handleReturnExpiryDate = useCallback((result: dayjs.Dayjs) => { + if (result) { + setValue("expiryDate", dayjsToDateString(result)); + } + }, [setValue]); + +return ( + <> {/* @@ -250,6 +274,44 @@ const FgStockInForm: React.FC = ({ />) } + + { + return ( + + { + if (!date) return; + setValue( + "productionDate", + date.format(INPUT_DATE_FORMAT), + ); + }} + inputRef={field.ref} + slotProps={{ + textField: { + error: Boolean(errors.productionDate?.message), + helperText: errors.productionDate?.message, + }, + }} + /> + + ); + }} + /> + + {/* {putawayMode || (<> {/* {putawayMode || (<> = ({ dateAdapter={AdapterDayjs} adapterLocale={`${language}-hk`} > - { - if (!date) return; - console.log(date.format(INPUT_DATE_FORMAT)); - setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); - // field.onChange(date); - }} - inputRef={field.ref} - slotProps={{ - textField: { - // required: true, - error: Boolean(errors.expiryDate?.message), - helperText: errors.expiryDate?.message, - }, - }} - /> + { + if (!date) return; + console.log(date.format(INPUT_DATE_FORMAT)); + setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); + }} + inputRef={field.ref} + open={openExpDatePicker && !openModal} + onOpen={() => setOpenExpDatePicker(true)} + onClose={() => setOpenExpDatePicker(false)} + slotProps={{ + textField: { + InputProps: { + ...(!disabled && { + endAdornment: ( + + + + ), + }) + }, + error: Boolean(errors.expiryDate?.message), + helperText: errors.expiryDate?.message, + onClick: () => setOpenExpDatePicker(true), + }, + }} + /> ); }} @@ -442,6 +523,13 @@ const FgStockInForm: React.FC = ({ */} - ); + + +); }; export default FgStockInForm; diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index b7b031b..0bb88ce 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -1,5 +1,4 @@ { - "dashboard": "資訊展示面板", "Edit": "編輯", "Job Order Production Process": "工單生產流程", @@ -7,12 +6,28 @@ "Search Criteria": "搜尋條件", "All": "全部", "No options": "沒有選項", + "Finished QC Job Orders": "完成QC工單", + "Reset": "重置", "Search": "搜尋", + "Staff No Required": "員工編號必填", + "User Not Found": "用戶不存在", + "Time Remaining": "剩餘時間", + "Select Printer": "選擇打印機", + "Finished Time": "完成時間", + "Printer": "打印機", + "Finished Qc Job Order List": "完成QC工單列表", + "Total finished Qc Job Order": "總完成QC工單數量", + "Timer Paused": "計時器已暫停", + "User not found with staffNo:": "用戶不存在", + "Total finished QC job orders": "總完成QC工單數量", + "Over Time": "超時", "Code": "編號", + "Staff No": "員工編號", "code": "編號", "Name": "名稱", "Assignment successful": "分配成功", + "Pass": "通過", "Unable to get user ID": "無法獲取用戶ID", "Unknown error: ": "未知錯誤: ", "Please try again later.": "請稍後重試。", @@ -25,7 +40,6 @@ "R&D": "研發", "STF": "樣品", "Other": "其他", - "Add some entries!": "添加條目", "Add Record": "新增", "Clean Record": "重置", @@ -49,19 +63,42 @@ "Changeover Time": "生產後轉換時間", "Warehouse": "倉庫", "Supplier": "供應商", - "Purchase Order":"採購單", - "Demand Forecast":"需求預測", + "Purchase Order": "採購單", + "Demand Forecast": "需求預測", "Pick Order": "提料單", - "Deliver Order":"送貨訂單", - "Project":"專案", - "Product":"產品", - "Material":"材料", - "mat":"原料", + "Deliver Order": "送貨訂單", + "Project": "專案", + "Product": "產品", + "Material": "材料", + "mat": "原料", "consumables": "消耗品", "non-consumables": "非消耗品", "fg": "成品", "sfg": "半成品", "item": "貨品", + "FG": "成品", + "Qty": "數量", + "FG & Material Demand Forecast Detail": "成品及材料需求預測詳情", + "View item In-out And inventory Ledger": "查看物料出入庫及庫存日誌", + "Delivery Order": "送貨訂單", + "Detail Scheduling": "詳細排程", + "Customer": "客戶", + "qcItem": "品檢項目", + "Item": "成品/半成品", + "Today": "今天", + "Yesterday": "昨天", + "Input Equipment is not match with process": "輸入的設備與流程不匹配", + "Staff No is required": "員工編號必填", + + "Day Before Yesterday": "前天", + "Select Date": "選擇日期", + "Production Date": "生產日期", + "QC Check Item": "QC品檢項目", + "QC Category": "QC品檢模板", + "qcCategory": "品檢模板", + "QC Check Template": "QC檢查模板", + "Mail": "郵件", + "Import Testing": "匯入測試", "FG":"成品", "Qty":"數量", "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", @@ -87,7 +124,7 @@ "Qc Item": "QC 項目", "FG Production Schedule": "FG 生產排程", "Inventory": "庫存", - "scheduling":"排程", + "scheduling": "排程", "settings": "設定", "items": "物料", "edit":"編輯", @@ -95,6 +132,11 @@ "Edit Equipment":"設備詳情", "equipmentType":"設備種類", "Description":"描述", + "edit": "編輯", + "Edit Equipment Type": "設備類型詳情", + "Edit Equipment": "設備詳情", + "equipmentType": "設備類型", + "Description": "描述", "Details": "詳情", "Equipment Type Details":"設備類型詳情", "Equipment Type":"設備類型", @@ -103,6 +145,12 @@ "Equipment Details":"設備詳情", "Exclude Date":"排除日期", "Finished Goods Name":"成品名稱", + "Equipment Type Details": "設備類型詳情", + "Save": "儲存", + "Cancel": "取消", + "Equipment Details": "設備詳情", + "Exclude Date": "排除日期", + "Finished Goods Name": "成品名稱", "create": "新增", "hr": "小時", "hrs": "小時", @@ -125,7 +173,6 @@ "Stop Scan": "停止掃碼", "Scan Result": "掃碼結果", "Expiry Date": "有效期", - "Pick Order Code": "提料單編號", "Target Date": "需求日期", "Lot Required Pick Qty": "批號需求數量", @@ -135,8 +182,6 @@ "No data available": "沒有資料", "jodetail": "工單細節", "Sign out": "登出", - - "By-product": "副產品", "Complete Step": "完成步驟", "Defect": "不良品", @@ -163,7 +208,6 @@ "Output Qty": "輸出數量", "Pending": "待處理", "pending": "待處理", - "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", "Please scan operator code": "請掃描操作員編號", "Please scan operator code first": "請先掃描操作員編號", @@ -171,15 +215,13 @@ "Production Process Information": "生產流程信息", "Production Process Steps": "生產流程步驟", "Scan Operator & Equipment": "掃描操作員和設備", - "Seq": "序號", "Setup Time (mins)": "生產前預備時間(分鐘)", "Start": "開始", "Start QR Scan": "開始掃碼", - "Status": "狀態", + "Status": "狀態", "in_progress": "進行中", "In_Progress": "進行中", "inProgress": "進行中", - "Step Name": "名稱", "Stop QR Scan": "停止掃碼", "Submit & Start": "提交並開始", @@ -188,10 +230,13 @@ "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", "View": "查看", "Back": "返回", - "BoM Material": "物料清單", + "BoM Material": "成品/半成品清單", "N/A": "不適用", - "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", - "Item Code": "物料編號", + "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度", + "Item Code": "成品/半成品名稱", + "Please scan equipment code": "請掃描設備編號", + "Equipment Code": "設備編號", + "Seq": "步驟", "Item Name": "物料名稱", "Job Order Info": "工單信息", "Matching Stock": "工單對料", @@ -223,6 +268,7 @@ "View Details": "查看詳情", "view stockin": "品檢", "No completed Job Order pick orders with matching found": "沒有相關記錄", + "Handler": "提料員", "Completed Step": "完成步驟", "Continue": "繼續", "Executing": "執行中", @@ -235,4 +281,4 @@ "Lines with sufficient stock: ": "可提料項目數量: ", "Lines with insufficient stock: ": "未能提料項目數量: ", "Total lines: ": "總數量:" -} +} \ No newline at end of file diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 95f4e5b..cd7998a 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -8,11 +8,19 @@ "Code": "工單編號", "Name": "成品/半成品名稱", "Picked Qty": "已提料數量", - "Req. Qty": "需求數量", + "Confirm All": "確認所有提料", "UoM": "銷售單位", "No": "沒有", + "User not found with staffNo:": "用戶不存在", + "Time Remaining": "剩餘時間", + "Over Time": "超時", + "Staff No:": "員工編號:", + "Timer Paused": "計時器已暫停", + "Staff No Required": "員工編號必填", + "Staff No": "員工編號", "Status": "工單狀態", "Lot No.": "批號", + "Pass": "通過", "Delete Job Order": "刪除工單", "Bom": "半成品/成品編號", "Release": "放單", @@ -86,6 +94,8 @@ "Job Order Item Name": "工單物料名稱", "Job Order Code": "工單編號", "View Details": "查看詳情", + "Skip": "跳過", + "Handler": "提料員", "Required Qty": "需求數量", "completed Job Order pick orders with Matching": "工單已完成提料和對料", "No completed Job Order pick orders with matching found": "沒有相關記錄", @@ -134,7 +144,7 @@ "Confirm Lot Substitution": "確認批號替換", "Processing...": "處理中", "Complete Job Order Record": "已完成工單記錄", - "Back": "返回", + "Lot Details": "批號細節", "No lot details available": "沒有批號細節", "Second Scan Completed": "對料已完成", @@ -146,7 +156,9 @@ "Reject": "拒絕", "Stock Unit": "庫存單位", "Group": "組", - "Item": "物料", + "Input Equipment is not match with process": "輸入的設備與流程不匹配", + "Item": "成品/半成品", + "Select Date": "選擇日期", "No Group": "沒有組", "No created items": "沒有創建物料", "Order Quantity": "需求數量", @@ -274,7 +286,6 @@ "acceptQty must not greater than": "接受數量不能大於", "escalation": "升級", "failedQty": "失敗數量", - "qcItem": "QC物料", "qcResult": "QC結果", "remarks": "備註", "supervisor": "主管", @@ -324,13 +335,15 @@ "pending": "待處理", "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", + "Please scan equipment code": "請掃描設備編號", + "Equipment Code": "設備編號", "Please scan operator code": "請掃描操作員編號", "Please scan operator code first": "請先掃描操作員編號", "Processing Time (mins)": "步驟時間(分鐘)", "Production Process Information": "生產流程信息", "Production Process Steps": "生產流程步驟", "Scan Operator & Equipment": "掃描操作員和設備", - "Seq": "序號", + "Seq:": "步驟", "Setup Time (mins)": "生產前預備時間(分鐘)", "Start": "開始", "Start QR Scan": "開始掃碼", @@ -356,18 +369,13 @@ "View": "查看", "Back": "返回", "N/A": "不適用", - "BoM Material": "物料清單", + "BoM Material": "成品/半成品清單", "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", - "Item Code": "物料編號", - "Item Name": "物料名稱", "Enter the number of cartons: ": "請輸入箱數:", "Number of cartons": "箱數", "You need to enter a number": "您需要輸入一個數字", "Number must be at least 1": "數字必須至少為1", - "Confirm": "確認", - "Cancel": "取消", - "Print Pick Record": "打印板頭紙", - "Printed Successfully.": "成功列印", + "Job Order Info": "工單信息", "Matching Stock": "工單對料", "No data found": "沒有找到資料",