# Conflicts: # src/i18n/zh/common.jsonmaster
| @@ -25,10 +25,13 @@ const JoEdit: React.FC<Props> = 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(); | |||
| } | |||
| @@ -8,6 +8,7 @@ export interface BomCombo { | |||
| label: string; | |||
| outputQty: number; | |||
| outputQtyUom: string; | |||
| description: string; | |||
| } | |||
| export const preloadBomCombo = (() => { | |||
| @@ -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<getTicketReleaseTable[]>( | |||
| `${BASE_API_URL}/doPickOrder/ticket-release-table`, | |||
| `${BASE_API_URL}/doPickOrder/ticket-release-table/${startDate}&${endDate}`, | |||
| { | |||
| method: "GET", | |||
| } | |||
| @@ -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<any>( | |||
| `${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<any>( | |||
| `${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<AllJoborderProductProcessInfoResponse[]>( | |||
| `${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<any>( | |||
| `${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<JobOrderListForPrintQrCodeResponse[]>( | |||
| `${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<any[]>( | |||
| @@ -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<FileResponse> => { | |||
| const reportBlob = await serverFetchBlob<FileResponse>( | |||
| `${BASE_API_URL}/jo/FGStockInLabel`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| return reportBlob; | |||
| }; | |||
| @@ -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<ReleaseProdScheduleRsp>( | |||
| `${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<SaveProdScheduleResponse>( | |||
| `${BASE_API_URL}/productionSchedule/detail/detailed/save`, | |||
| @@ -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 { | |||
| @@ -128,6 +128,7 @@ export interface StockInLine { | |||
| dnNo?: string; | |||
| dnDate?: number[]; | |||
| stockQty?: number; | |||
| bomDescription?: string; | |||
| handlerId?: number; | |||
| putAwayLines?: PutAwayLine[]; | |||
| qcResult?: QcResult[]; | |||
| @@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ type, defaultInputs }) => { | |||
| setFilteredSchedules((fs) => | |||
| orderBy( | |||
| uniqBy([...fs, ...response.records], "id"), | |||
| ["id"], ["desc"])); | |||
| ["id"], ["asc"])); | |||
| break; | |||
| } | |||
| } | |||
| @@ -298,20 +300,67 @@ const DSOverview: React.FC<Props> = ({ 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 ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="flex-end" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| spacing={2} // This provides consistent space between buttons | |||
| sx={{ mb: 3 }} // Adds some margin below the button group | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| variant="outlined" // Outlined variant makes it look distinct from the primary action | |||
| color="primary" | |||
| startIcon={<CalendarMonth />} | |||
| onClick={testDetailedScheduleClick} | |||
| // disabled={filteredSchedules.some(ele => arrayToDayjs(ele.scheduleAt).isToday())} | |||
| > | |||
| {t("Test Detailed Schedule")} | |||
| {t("Detailed Schedule")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" // Solid button for the "Export" action | |||
| color="success" // Green color often signifies a successful action/download | |||
| startIcon={<FileDownload />} | |||
| onClick={exportProdScheduleClick} | |||
| sx={{ | |||
| boxShadow: 2, | |||
| '&:hover': { backgroundColor: 'success.dark', boxShadow: 4 } | |||
| }} | |||
| > | |||
| {t("Export Schedule")} | |||
| </Button> | |||
| </Stack> | |||
| <SearchBox | |||
| @@ -28,7 +28,9 @@ import ViewByFGDetails, { | |||
| // FGRecord, | |||
| } from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | |||
| import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | |||
| import { releaseProdScheduleLine, saveProdScheduleLine } from "@/app/api/scheduling/actions"; | |||
| // NOTE: Assuming 'releaseProdSchedule' is the new action function | |||
| // you need to implement to call the '/productionSchedule/detail/detailed/release' API | |||
| import { releaseProdScheduleLine, saveProdScheduleLine, releaseProdSchedule } from "@/app/api/scheduling/actions"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
| @@ -58,7 +60,8 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| // 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<Props> = ({ | |||
| }); | |||
| const errors = formProps.formState.errors; | |||
| const { reset, handleSubmit, setValue, getValues } = formProps | |||
| useEffect(() => { | |||
| if (defaultValues) { | |||
| reset(defaultValues); | |||
| } | |||
| }, [defaultValues, reset]); | |||
| const lineFormProps = useFieldArray<DetailedProdScheduleResult>({ | |||
| control: formProps.control, | |||
| name: "prodScheduleLines" | |||
| @@ -138,32 +149,64 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| }) | |||
| 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<string | number | null>(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<Props> = ({ | |||
| 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<Props> = ({ | |||
| 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<Props> = ({ | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| {/*<Grid>*/} | |||
| {/* <Typography mb={2} variant="h4">*/} | |||
| {/* {t(`${mode} ${title}`)}*/} | |||
| {/* </Typography>*/} | |||
| {/* <Typography mb={2} variant="h4">*/} | |||
| {/* {t(`${mode} ${title}`)}*/} | |||
| {/* </Typography>*/} | |||
| {/*</Grid>*/} | |||
| <DetailInfoCard | |||
| // recordDetails={formProps.formState.defaultValues} | |||
| @@ -210,26 +255,23 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| isEditing={false} | |||
| /> | |||
| {/* <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| onClick={onClickEdit} | |||
| // startIcon={<Add />} | |||
| //LinkComponent={Link} | |||
| //href="qcCategory/create" | |||
| > | |||
| {isEdit ? t("Save") : t("Edit")} | |||
| </Button> | |||
| </Stack> */} | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| onClick={onClickEdit} | |||
| > | |||
| {isEdit ? t("Save") : t("Edit")} | |||
| </Button> | |||
| </Stack> */} | |||
| {/* <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" /> | |||
| <Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" /> | |||
| </Tabs> */} | |||
| <Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" /> | |||
| <Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" /> | |||
| </Tabs> */} | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| @@ -247,12 +289,24 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| type={type} /> | |||
| {/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| {/* --- NEW BUTTON: Release Entire Schedule --- */} | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| onClick={onGlobalReleaseClick} | |||
| disabled={!scheduleId} // Disable if we don't have a schedule ID | |||
| > | |||
| {t("生成工單")} | |||
| </Button> | |||
| {/* ------------------------------------------- */} | |||
| {/* <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| // disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> */} | |||
| @@ -269,4 +323,4 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||
| </> | |||
| ); | |||
| }; | |||
| export default DetailedScheduleDetailView; | |||
| export default DetailedScheduleDetailView; | |||
| @@ -17,28 +17,34 @@ const DetailedScheduleDetailWrapper: React.FC<Props> & 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 ( | |||
| <DetailedScheduleDetailView | |||
| isEditMode={Boolean(id)} | |||
| defaultValues={prodSchedule} | |||
| type={type} | |||
| // qcChecks={qcChecks || []} | |||
| /> | |||
| ); | |||
| }; | |||
| DetailedScheduleDetailWrapper.Loading = GeneralLoading; | |||
| export default DetailedScheduleDetailWrapper; | |||
| export default DetailedScheduleDetailWrapper; | |||
| @@ -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<Props> = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => { | |||
| const ViewByFGDetails: React.FC<Props> = ({ | |||
| apiRef, | |||
| isEdit, | |||
| type, | |||
| onReleaseClick, | |||
| onEditClick, | |||
| handleEditChange, | |||
| onSaveClick, | |||
| onCancelClick | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| @@ -47,83 +47,20 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| const { | |||
| getValues, | |||
| watch, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| } = useFormContext<DetailedProdScheduleResult>(); | |||
| // 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<Column<DetailedProdScheduleLineResult>[]>( | |||
| () => [ | |||
| { | |||
| 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<Props> = ({ 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 <ProdTimeColumn prodTimeInMinute={row.prodTimeInMinute} /> | |||
| } | |||
| 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 ( | |||
| <Grid container spacing={2}> | |||
| {/* <Grid item xs={12} key={"all"}> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("FG Demand List (7 Days)")} | |||
| </Typography> | |||
| <EditableSearchResults<FGRecord> | |||
| index={7} | |||
| items={fakeOverallRecords} | |||
| columns={overallColumns} | |||
| setPagingController={updatePagingController} | |||
| pagingController={pagingController[7]} | |||
| isAutoPaging={false} | |||
| isEditable={false} | |||
| isEdit={isEdit} | |||
| hasCollapse={true} | |||
| /> | |||
| </Grid> */} | |||
| {/* {dayPeriod.map((date, index) => ( */} | |||
| <Grid item xs={12}> | |||
| {/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {`${t("FG Demand Date")}: ${date}`} | |||
| </Typography> */} | |||
| <ScheduleTable<DetailedProdScheduleLineResult> | |||
| 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<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| onCancelClick={onCancelClick} | |||
| /> | |||
| </Grid> | |||
| {/* ))} */} | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default ViewByFGDetails; | |||
| }; // Added missing closing brace | |||
| export default ViewByFGDetails; | |||
| @@ -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<Props> = ({ | |||
| open, | |||
| bomCombo, | |||
| jobTypes, | |||
| onClose, | |||
| onSearch, | |||
| }) => { | |||
| @@ -30,19 +33,130 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| const formProps = useForm<SaveJo>({ | |||
| 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<Element, Event>, 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<Element, Event>, 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<Props> = ({ | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| "flex-direction": "column", | |||
| flexDirection: "column", | |||
| padding: "20px", | |||
| height: "100%", //'30rem', | |||
| width: "100%", | |||
| @@ -199,36 +313,42 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={6}> | |||
| <Controller | |||
| control={control} | |||
| name="jobTypeId" | |||
| rules={{ required: t("Job Type required!") as string }} | |||
| render={({ field, fieldState: { error } }) => ( | |||
| <FormControl fullWidth error={Boolean(error)}> | |||
| <InputLabel>{t("Job Type")}</InputLabel> | |||
| <Select | |||
| <Controller | |||
| control={control} | |||
| name="jobTypeId" | |||
| rules={{ required: t("Job Type required!") as string }} | |||
| render={({ field, fieldState: { error } }) => { | |||
| //console.log("Job Type Select render - filteredJobTypes:", filteredJobTypes); | |||
| //console.log("Current field.value:", field.value); | |||
| return ( | |||
| <FormControl fullWidth error={Boolean(error)}> | |||
| <InputLabel>{t("Job Type")}</InputLabel> | |||
| <Select | |||
| {...field} | |||
| label={t("Job Type")} | |||
| value={field.value?.toString() ?? ""} | |||
| onChange={(event) => { | |||
| const value = event.target.value; | |||
| console.log("Job Type changed to:", value); | |||
| field.onChange(value === "" ? undefined : Number(value)); | |||
| }} | |||
| > | |||
| > | |||
| <MenuItem value=""> | |||
| <em>{t("Please select")}</em> | |||
| </MenuItem> | |||
| <MenuItem value="1">{t("FG")}</MenuItem> | |||
| <MenuItem value="2">{t("WIP")}</MenuItem> | |||
| <MenuItem value="3">{t("R&D")}</MenuItem> | |||
| <MenuItem value="4">{t("STF")}</MenuItem> | |||
| <MenuItem value="5">{t("Other")}</MenuItem> | |||
| </Select> | |||
| {/*{error && <FormHelperText>{error.message}</FormHelperText>}*/} | |||
| </FormControl> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| {/* {filteredJobTypes.map((jobType) => (*/} | |||
| {jobTypes.map((jobType) => ( | |||
| <MenuItem key={jobType.id} value={jobType.id.toString()}> | |||
| {t(jobType.name)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={6}> | |||
| <Controller | |||
| control={control} | |||
| @@ -398,6 +398,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| <JoCreateFormModal | |||
| open={isCreateJoModalOpen} | |||
| bomCombo={bomCombo} | |||
| jobTypes={jobTypes} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={() => { | |||
| setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 | |||
| @@ -47,7 +47,10 @@ const JoPickOrderList: React.FC<Props> = ({ 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<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} /> | |||
| <JobPickExecution | |||
| filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | |||
| //onSwitchToRecordTab={onSwitchToRecordTab} | |||
| onBackToList={handleBackToList} // 传递新的回调 | |||
| /> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -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 = { | |||
| @@ -487,7 +487,8 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, onBack }) => { | |||
| )} | |||
| {/* Combined Lot Table */} | |||
| <Box> | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| onClick={handleSubmitAllScanned} | |||
| disabled={isSubmittingAll} | |||
| sx={{ minWidth: '160px' }} | |||
| > | |||
| {isSubmittingAll ? ( | |||
| <> | |||
| <CircularProgress size={16} sx={{ mr: 1 }} /> | |||
| {t("Submitting...")} | |||
| </> | |||
| ) : ( | |||
| t("Confirm All") | |||
| )} | |||
| </Button> | |||
| {/* | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}> | |||
| {!isManualScanning ? ( | |||
| @@ -1166,18 +1192,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| */} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Index")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Handler")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| <TableCell align="center">{t("Scan Result")}</TableCell> | |||
| {/* <TableCell align="center">{t("Scan Result")}</TableCell> */} | |||
| <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -1212,6 +1239,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| {lot.routerRoute || '-'} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell>{lot.handler || '-'}</TableCell> | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | |||
| <TableCell> | |||
| @@ -1232,7 +1260,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; | |||
| })()} | |||
| </TableCell> | |||
| {/* | |||
| <TableCell align="center"> | |||
| {lot.matchStatus?.toLowerCase() === 'scanned' || | |||
| lot.matchStatus?.toLowerCase() === 'completed' ? ( | |||
| @@ -1266,7 +1294,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| </Typography> | |||
| )} | |||
| </TableCell> | |||
| */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ display: 'flex', justifyContent: 'center' }}> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| @@ -1277,9 +1305,10 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ filterArgs, onBack }) => { | |||
| minWidth: '70px' | |||
| }} | |||
| > | |||
| {t("Submit")} | |||
| {t("Confirm")} | |||
| </Button> | |||
| <Button | |||
| @@ -15,7 +15,7 @@ import { | |||
| import { | |||
| arrayToDayjs, | |||
| } from "@/app/utils/formatUtil"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box, TextField, Autocomplete } from "@mui/material"; | |||
| import Jodetail from "./Jodetail" | |||
| import PickExecution from "./JobPickExecution"; | |||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
| @@ -63,12 +63,18 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders, printerCombo }) => { | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); | |||
| const [hasDataTab0, setHasDataTab0] = useState(false); | |||
| const [hasDataTab1, setHasDataTab1] = useState(false); | |||
| const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| //const [printers, setPrinters] = useState<PrinterCombo[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); | |||
| const [hasDataTab0, setHasDataTab0] = useState(false); | |||
| const [hasDataTab1, setHasDataTab1] = useState(false); | |||
| const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| // Add printer selection state | |||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| const [printQty, setPrintQty] = useState<number>(1); | |||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | |||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | |||
| ); | |||
| @@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener); | |||
| }; | |||
| }, []); | |||
| /* | |||
| useEffect(() => { | |||
| const fetchPrinters = async () => { | |||
| try { | |||
| // 需要创建一个客户端版本的 fetchPrinterCombo | |||
| // 或者使用 API 路由 | |||
| // const printersData = await fetch('/api/printers/combo').then(r => r.json()); | |||
| // setPrinters(printersData); | |||
| } catch (error) { | |||
| console.error("Error fetching printers:", error); | |||
| } | |||
| }; | |||
| fetchPrinters(); | |||
| }, []); | |||
| */ | |||
| useEffect(() => { | |||
| const onAssigned = () => { | |||
| localStorage.removeItem('hideCompletedUntilNext'); | |||
| @@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| window.addEventListener('pickOrderAssigned', onAssigned); | |||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | |||
| }, []); | |||
| // ... existing code ... | |||
| useEffect(() => { | |||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||
| @@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| return () => { | |||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| }; | |||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||
| }, [tabIndex]); | |||
| // 新增:处理标签页切换时的打印按钮状态重置 | |||
| useEffect(() => { | |||
| @@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| } | |||
| }, [tabIndex]); | |||
| // ... existing code ... | |||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| @@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| return ( | |||
| <Box sx={{ | |||
| height: '100vh', // Full viewport height | |||
| overflow: 'auto' // Single scrollbar for the whole page | |||
| height: '100vh', | |||
| overflow: 'auto' | |||
| }}> | |||
| {/* Header section */} | |||
| <Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}> | |||
| <Stack rowGap={2}> | |||
| <Grid container alignItems="center"> | |||
| <Grid item xs={8}> | |||
| </Grid> | |||
| {/* Last 2 buttons aligned right | |||
| <Grid item xs={6} > | |||
| {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( | |||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | |||
| <Typography variant="h6" gutterBottom> | |||
| {t("Unassigned Job Orders")} ({unassignedOrders.length}) | |||
| </Typography> | |||
| <Stack direction="row" spacing={1} flexWrap="wrap"> | |||
| {unassignedOrders.map((order) => ( | |||
| <Button | |||
| key={order.pickOrderId} | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handleAssignOrder(order.pickOrderId)} | |||
| disabled={isLoadingUnassigned} | |||
| > | |||
| {order.pickOrderCode} - {order.jobOrderName} | |||
| </Button> | |||
| ))} | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| </Grid> | |||
| */} | |||
| {/* Header section with printer selection */} | |||
| <Box sx={{ | |||
| p: 1, | |||
| borderBottom: '1px solid #e0e0e0', | |||
| minHeight: 'auto', | |||
| display: 'flex', | |||
| alignItems: 'center', | |||
| justifyContent: 'space-between', | |||
| gap: 2, | |||
| flexWrap: 'wrap', | |||
| }}> | |||
| {/* Left side - Title */} | |||
| </Grid> | |||
| </Stack> | |||
| </Box> | |||
| {/* Right side - Printer selection (only show on tab 1) */} | |||
| {tabIndex === 1 && ( | |||
| <Stack | |||
| direction="row" | |||
| spacing={2} | |||
| sx={{ | |||
| alignItems: 'center', | |||
| flexWrap: 'wrap', | |||
| rowGap: 1, | |||
| }} | |||
| > | |||
| <Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| options={printerCombo || []} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 200 }} | |||
| size="small" | |||
| renderInput={(params) => ( | |||
| <TextField {...params} placeholder={t("Printer")} /> | |||
| )} | |||
| /> | |||
| <Typography variant="body2" sx={{ minWidth: 'fit-content', ml: 1 }}> | |||
| {t("Print Quantity")}: | |||
| </Typography> | |||
| <TextField | |||
| type="number" | |||
| label={t("Print Quantity")} | |||
| value={printQty} | |||
| onChange={(e) => { | |||
| const value = parseInt(e.target.value) || 1; | |||
| setPrintQty(Math.max(1, value)); | |||
| }} | |||
| inputProps={{ min: 1, step: 1 }} | |||
| sx={{ width: 120 }} | |||
| size="small" | |||
| /> | |||
| </Stack> | |||
| )} | |||
| </Box> | |||
| {/* Tabs section - Move the click handler here */} | |||
| {/* Tabs section */} | |||
| <Box sx={{ | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| {/* <Tab label={t("Pick Order Detail")} iconPosition="end" /> */} | |||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | |||
| {/* <Tab label={t("Job Order Match")} iconPosition="end" /> */} | |||
| {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | |||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Box> | |||
| {/* Content section - NO overflow: 'auto' here */} | |||
| <Box sx={{ | |||
| p: 2 | |||
| }}> | |||
| {/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */} | |||
| {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | |||
| {/* Content section */} | |||
| <Box sx={{ p: 2 }}> | |||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||
| {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | |||
| {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | |||
| {tabIndex === 1 && ( | |||
| <CompleteJobOrderRecord | |||
| filterArgs={filterArgs} | |||
| printerCombo={printerCombo} | |||
| selectedPrinter={selectedPrinter} | |||
| printQty={printQty} | |||
| /> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| 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<Props> = ({ filterArgs ,printerCombo}) => { | |||
| const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| 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<Props> = ({ filterArgs ,printerCombo}) => | |||
| // 修改:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| //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 | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| const [printQty, setPrintQty] = useState<number>(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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs ,printerCombo}) => | |||
| <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | |||
| </Typography> | |||
| </Stack> | |||
| {/* | |||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handlePickRecord(selectedJobOrderPickOrder)} | |||
| sx={{ mt: 1 }} | |||
| > | |||
| {t("Print Pick Record")} | |||
| </Button> | |||
| </Stack> | |||
| */} | |||
| </CardContent> | |||
| </Card> | |||
| @@ -545,12 +539,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| height: '100%' | |||
| }}> | |||
| <Checkbox | |||
| checked={lot.secondQrScanStatus === 'completed'} | |||
| checked={lot.match_status === 'completed'} | |||
| disabled={true} | |||
| readOnly={true} | |||
| size="large" | |||
| sx={{ | |||
| color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400', | |||
| color: lot.match_status === 'completed' ? 'success.main' : 'grey.400', | |||
| '&.Mui-checked': { | |||
| color: 'success.main', | |||
| }, | |||
| @@ -600,37 +594,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | |||
| </Typography> | |||
| <Box sx={{ mb: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1, bgcolor: 'background.paper' }}> | |||
| <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap"> | |||
| <Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| options={availablePrinters} | |||
| getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`} | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 250 }} | |||
| size="small" | |||
| renderInput={(params) => <TextField {...params} label={t("Printer")} />} | |||
| /> | |||
| <Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}> | |||
| {t("Print Quantity")}: | |||
| </Typography> | |||
| <TextField | |||
| type="number" | |||
| label={t("Print Quantity")} | |||
| value={printQty} | |||
| onChange={(e) => { | |||
| const value = parseInt(e.target.value) || 1; | |||
| setPrintQty(Math.max(1, value)); | |||
| }} | |||
| inputProps={{ min: 1, step: 1 }} | |||
| sx={{ width: 120 }} | |||
| size="small" | |||
| /> | |||
| </Stack> | |||
| </Box> | |||
| {/* 列表 */} | |||
| {filteredJobOrderPickOrders.length === 0 ? ( | |||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||
| @@ -652,7 +616,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||
| {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} | |||
| {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | |||
| @@ -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<string, any>; | |||
| onSwitchToRecordTab: () => void; | |||
| //onSwitchToRecordTab: () => void; | |||
| onBackToList?: () => void; | |||
| } | |||
| // QR Code Modal Component (from GoodPickExecution) | |||
| @@ -324,7 +323,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) => { | |||
| const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setQrScanSuccess(false); | |||
| } | |||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||
| return; // ✅ 直接返回,不再调用后面的分支 | |||
| } | |||
| // Case 2: Same item, different lot - show confirmation modal | |||
| @@ -977,7 +985,7 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| checkAndAutoAssignNext(); | |||
| if (onSwitchToRecordTab) { | |||
| onSwitchToRecordTab(); | |||
| if (onBackToList) { | |||
| onBackToList(); | |||
| } | |||
| }, 2000); | |||
| } else { | |||
| @@ -1380,7 +1396,7 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| </Box> | |||
| </Box> | |||
| {qrScanError && !qrScanSuccess && ( | |||
| <Alert severity="error" sx={{ mb: 2 }}> | |||
| {qrScanError && !qrScanSuccess && ( | |||
| <Alert | |||
| severity="error" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| color: "error.main", // ✅ 整个 Alert 文字用错误红 | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| // color: "error.main", // ✅ 明确指定 message 文字颜色 | |||
| }, | |||
| "& .MuiSvgIcon-root": { | |||
| color: "error.main", // 图标继续红色(可选) | |||
| }, | |||
| backgroundColor: "error.light", | |||
| }} | |||
| > | |||
| {t("QR code does not match any item in current orders.")} | |||
| </Alert> | |||
| )} | |||
| {qrScanSuccess && ( | |||
| <Alert severity="success" sx={{ mb: 2 }}> | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| {qrScanSuccess && ( | |||
| <Alert | |||
| severity="success" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| // 背景用很浅的绿色 | |||
| bgcolor: "rgba(76, 175, 80, 0.08)", | |||
| // 文字用主题 success 绿 | |||
| color: "success.main", | |||
| // 去掉默认强烈的色块感 | |||
| "& .MuiAlert-icon": { | |||
| color: "success.main", | |||
| }, | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| color: "success.main", | |||
| }, | |||
| }} | |||
| > | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| @@ -1694,6 +1753,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| <TableRow> | |||
| <TableCell>{t("Index")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Handler")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| @@ -1733,6 +1793,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| {lot.routerRoute || '-'} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell>{lot.handler || '-'}</TableCell> | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | |||
| <TableCell> | |||
| @@ -1837,6 +1898,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| > | |||
| {t("Issue")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handleSkip(lot)} | |||
| disabled={lot.stockOutLineStatus === 'completed'} | |||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | |||
| > | |||
| {t("Skip")} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| </TableCell> | |||
| @@ -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<FinishedQcJobOrderListProps> = ({ | |||
| printerCombo, | |||
| selectedPrinter, | |||
| }) => { | |||
| const { t } = useTranslation(["common"]); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const [loading, setLoading] = useState(false); | |||
| const [jobOrders, setJobOrders] = useState<JobOrderListForPrintQrCodeResponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| const [isPrinting, setIsPrinting] = useState(false); | |||
| const [printingId, setPrintingId] = useState<number | null>(null); | |||
| const [selectedDate, setSelectedDate] = useState<string>("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 ( | |||
| <Box> | |||
| {/* Date Selector */} | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}> | |||
| <Box sx={{ maxWidth: 300 }}> | |||
| <FormControl fullWidth size="small"> | |||
| <InputLabel id="date-select-label">{t("Select Date")}</InputLabel> | |||
| <Select | |||
| labelId="date-select-label" | |||
| id="date-select" | |||
| value={selectedDate} | |||
| // label={t("Select Date")} | |||
| onChange={(e) => { | |||
| setSelectedDate(e.target.value); | |||
| }} | |||
| > | |||
| <MenuItem value="today"> | |||
| {t("Today")} ({getDateLabel(0)}) | |||
| </MenuItem> | |||
| <MenuItem value="yesterday"> | |||
| {t("Yesterday")} ({getDateLabel(1)}) | |||
| </MenuItem> | |||
| <MenuItem value="dayBeforeYesterday"> | |||
| {t("Day Before Yesterday")} ({getDateLabel(2)}) | |||
| </MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| </Stack> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total finished QC job orders")}: {jobOrders.length} | |||
| </Typography> | |||
| <TableContainer component={Paper} sx={{ boxShadow: 2 }}> | |||
| <Table sx={{ minWidth: 650 }}> | |||
| <TableHead> | |||
| <TableRow sx={{ bgcolor: "grey.50" }}> | |||
| <TableCell sx={{ fontWeight: "bold" }}>{t("Code")}</TableCell> | |||
| <TableCell sx={{ fontWeight: "bold" }}>{t("Name")}</TableCell> | |||
| <TableCell sx={{ fontWeight: "bold" }}>{t("Required Qty")}</TableCell> | |||
| <TableCell sx={{ fontWeight: "bold" }}>{t("Finished Time")}</TableCell> | |||
| <TableCell sx={{ fontWeight: "bold" }} align="center">{t("Action")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {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 ( | |||
| <TableRow | |||
| key={jobOrder.id} | |||
| sx={{ | |||
| "&:last-child td, &:last-child th": { border: 0 }, | |||
| "&:hover": { bgcolor: "grey.50" }, | |||
| }} | |||
| > | |||
| <TableCell component="th" scope="row"> | |||
| {jobOrder.code} | |||
| </TableCell> | |||
| <TableCell>{jobOrder.name}</TableCell> | |||
| <TableCell>{jobOrder.reqQty}</TableCell> | |||
| <TableCell>{finishedTimeDisplay}</TableCell> | |||
| <TableCell align="center"> | |||
| <Tooltip title={t("Print QR Code")}> | |||
| <IconButton | |||
| color="primary" | |||
| onClick={() => handlePrint(jobOrder)} | |||
| disabled={isPrinting || printerCombo.length <= 0 || !selectedPrinter} | |||
| size="small" | |||
| > | |||
| {isCurrentlyPrinting ? ( | |||
| <CircularProgress size={20} /> | |||
| ) : ( | |||
| <QrCodeIcon /> | |||
| )} | |||
| </IconButton> | |||
| </Tooltip> | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| })} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| {jobOrders.length > 0 && ( | |||
| <TablePagination | |||
| component="div" | |||
| count={jobOrders.length} | |||
| page={page} | |||
| rowsPerPage={PER_PAGE} | |||
| onPageChange={(e, p) => setPage(p)} | |||
| rowsPerPageOptions={[PER_PAGE]} | |||
| /> | |||
| )} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default FinishedQcJobOrderList; | |||
| @@ -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<ProductProcessDetailProps> = ({ | |||
| 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<ProductProcessDetailProps> = ({ | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | |||
| const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null); | |||
| const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null); | |||
| // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null); | |||
| const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null); | |||
| // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState<number | null>(null); | |||
| const [scannedEquipmentCode, setScannedEquipmentCode] = useState<string | null>(null); | |||
| const [scanningLineId, setScanningLineId] = useState<number | null>(null); | |||
| const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(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) => { | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {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") | |||
| } | |||
| </Typography> | |||
| </Box> | |||
| @@ -792,7 +807,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | |||
| disabled={!scannedStaffNo} | |||
| disabled={!scannedStaffNo } | |||
| > | |||
| {t("Submit & Start")} | |||
| </Button> | |||
| @@ -115,7 +115,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| }, [fetchData]); | |||
| // PickTable 组件内容 | |||
| const getStockAvailable = (line: JobOrderLine) => { | |||
| 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 <Typography sx={{ fontSize: "18px" }}>{params.value}</Typography>; | |||
| return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>; | |||
| }, | |||
| }, | |||
| { | |||
| @@ -325,16 +326,28 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| align: "left", | |||
| headerAlign: "left", | |||
| renderCell: (params) => { | |||
| return <Typography sx={{ fontSize: "18px" }}>{params.value || ""}</Typography>; | |||
| return( | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography sx={{ fontWeight: 500 }}> </Typography> | |||
| <Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography> | |||
| <Typography sx={{ fontWeight: 500 }}> </Typography> | |||
| </Box> | |||
| ) | |||
| }, | |||
| }, | |||
| ]; | |||
| 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) => { | |||
| /> | |||
| </Box> | |||
| ); | |||
| const ProductionProcessesLineRemarkTableContent = () => ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <ProcessSummaryHeader processData={processData} /> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| }} | |||
| disableColumnMenu | |||
| rows={productionProcessesLineRemarkTableRows ?? []} | |||
| columns={productionProcessesLineRemarkTableColumns} | |||
| getRowHeight={() => 'auto'} | |||
| /> | |||
| </Box> | |||
| ); | |||
| const ProductionProcessesLineRemarkTableContent = () => ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <ProcessSummaryHeader processData={processData} /> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| // ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem) | |||
| "& .MuiDataGrid-cell": { | |||
| fontSize: "0.875rem", // ✅ Match default body2 size | |||
| fontWeight: 500, | |||
| }, | |||
| "& .MuiDataGrid-columnHeader": { | |||
| fontSize: "0.875rem", // ✅ Match header size | |||
| fontWeight: 600, | |||
| }, | |||
| // ✅ Ensure empty columns are visible | |||
| "& .MuiDataGrid-columnHeaders": { | |||
| display: "flex", | |||
| }, | |||
| "& .MuiDataGrid-row": { | |||
| display: "flex", | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| rows={productionProcessesLineRemarkTableRows ?? []} | |||
| columns={productionProcessesLineRemarkTableColumns} | |||
| getRowHeight={() => 'auto'} | |||
| hideFooter={false} // ✅ Ensure footer is visible | |||
| /> | |||
| </Box> | |||
| ); | |||
| return ( | |||
| @@ -94,7 +94,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ 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<ProductProcessListProps> = ({ 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<ProductProcessListProps> = ({ onSelectProcess | |||
| </Stack> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Item Name")}: {process.itemName} | |||
| {t("Item Name")}: {process.itemCode} {process.itemName} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Required Qty")}: {process.requiredQty} | |||
| {t("Required Qty")}: {process.requiredQty} {process.uom} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} | |||
| @@ -268,7 +270,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| disabled={process.assignedTo != null || process.matchStatus == "completed"} | |||
| disabled={process.assignedTo != null || process.matchStatus == "completed"|| process.pickOrderStatus != "completed"} | |||
| onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)} | |||
| > | |||
| {t("Matching Stock")} | |||
| @@ -2,15 +2,19 @@ | |||
| import React, { useState, useEffect, useCallback } from "react"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; | |||
| import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; | |||
| import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | |||
| import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | |||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | |||
| import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | |||
| import { | |||
| fetchProductProcesses, | |||
| fetchProductProcessesByJobOrderId, | |||
| ProductProcessLineResponse | |||
| } from "@/app/api/jo/actions"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type PrinterCombo = { | |||
| id: number; | |||
| value: number; | |||
| @@ -25,17 +29,25 @@ type PrinterCombo = { | |||
| interface ProductionProcessPageProps { | |||
| printerCombo: PrinterCombo[]; | |||
| } | |||
| const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | |||
| const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | |||
| const { t } = useTranslation(["common"]); | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| const [selectedMatchingStock, setSelectedMatchingStock] = useState<{ | |||
| jobOrderId: number; | |||
| productProcessId: number; | |||
| } | null>(null); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // Add printer selection state | |||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| // 从 sessionStorage 恢复状态(仅在客户端) | |||
| useEffect(() => { | |||
| if (typeof window !== 'undefined') { | |||
| @@ -76,6 +88,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| } | |||
| }, []); | |||
| const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => { | |||
| setTabIndex(newValue); | |||
| }, []); | |||
| if (selectedMatchingStock) { | |||
| return ( | |||
| <JobPickExecutionsecondscan | |||
| @@ -84,6 +100,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| /> | |||
| ); | |||
| } | |||
| if (selectedProcessId !== null) { | |||
| return ( | |||
| <ProductionProcessJobOrderDetail | |||
| @@ -94,21 +111,86 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| } | |||
| return ( | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0 | |||
| }); | |||
| }} | |||
| /> | |||
| <Box> | |||
| {/* Header section with printer selection */} | |||
| {tabIndex === 1 && ( | |||
| <Box sx={{ | |||
| p: 1, | |||
| borderBottom: '1px solid #e0e0e0', | |||
| minHeight: 'auto', | |||
| display: 'flex', | |||
| alignItems: 'center', | |||
| justifyContent: 'flex-end', | |||
| gap: 2, | |||
| flexWrap: 'wrap', | |||
| }}> | |||
| <Stack | |||
| direction="row" | |||
| spacing={2} | |||
| sx={{ | |||
| alignItems: 'center', | |||
| flexWrap: 'wrap', | |||
| rowGap: 1, | |||
| }} | |||
| > | |||
| <Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo || []} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter || undefined} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 200 }} | |||
| size="small" | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| placeholder={t("Printer")} | |||
| inputProps={{ | |||
| ...params.inputProps, | |||
| readOnly: true, | |||
| }} | |||
| /> | |||
| )} | |||
| /> | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Finished QC Job Orders")} /> | |||
| </Tabs> | |||
| {tabIndex === 0 && ( | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0 | |||
| }); | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| <FinishedQcJobOrderList | |||
| printerCombo={printerCombo} | |||
| selectedPrinter={selectedPrinter} | |||
| /> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -19,6 +19,8 @@ import { | |||
| CardContent, | |||
| Grid, | |||
| } from "@mui/material"; | |||
| import { Alert } from "@mui/material"; | |||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import StopIcon from "@mui/icons-material/Stop"; | |||
| @@ -75,6 +77,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | |||
| const [isOverTime, setIsOverTime] = useState(false); | |||
| const [frozenRemainingTime, setFrozenRemainingTime] = useState<string | null>(null); | |||
| const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null); | |||
| const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | |||
| const [pauseReason, setPauseReason] = useState(""); | |||
| // 检查是否两个都已扫描 | |||
| @@ -91,7 +96,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| fetchProductProcessLineDetail(lineId) | |||
| .then((detail) => { | |||
| setLineDetail(detail as any); | |||
| // 初始化 outputData 从 lineDetail | |||
| console.log("📋 Line Detail loaded:", { | |||
| id: detail.id, | |||
| status: detail.status, | |||
| durationInMinutes: detail.durationInMinutes, | |||
| startTime: detail.startTime, | |||
| startTimeType: typeof detail.startTime, | |||
| hasDuration: !!detail.durationInMinutes, | |||
| hasStartTime: !!detail.startTime, | |||
| }); | |||
| setOutputData(prev => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| @@ -112,27 +125,192 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| }); | |||
| }, [lineId]); | |||
| useEffect(() => { | |||
| // Don't show time remaining if completed | |||
| if (lineDetail?.status === "Completed") { | |||
| console.log("Line is completed"); | |||
| setRemainingTime(null); | |||
| setIsOverTime(false); | |||
| return; | |||
| } | |||
| // ✅ 问题1:添加详细的调试打印 | |||
| console.log("🔍 Time Remaining Debug:", { | |||
| lineId: lineDetail?.id, | |||
| equipmentId: lineDetail?.equipmentId, | |||
| equipmentType: lineDetail?.equipmentType, | |||
| durationInMinutes: lineDetail?.durationInMinutes, | |||
| startTime: lineDetail?.startTime, | |||
| startTimeType: typeof lineDetail?.startTime, | |||
| isStartTimeArray: Array.isArray(lineDetail?.startTime), | |||
| status: lineDetail?.status, | |||
| hasDuration: !!lineDetail?.durationInMinutes, | |||
| hasStartTime: !!lineDetail?.startTime, | |||
| }); | |||
| if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { | |||
| console.log("❌ Line duration or start time is not valid", { | |||
| durationInMinutes: lineDetail?.durationInMinutes, | |||
| startTime: lineDetail?.startTime, | |||
| equipmentId: lineDetail?.equipmentId, | |||
| equipmentType: lineDetail?.equipmentType, | |||
| }); | |||
| setRemainingTime(null); | |||
| setIsOverTime(false); | |||
| return; | |||
| } | |||
| const start = new Date(lineDetail.startTime as any); | |||
| const end = new Date(start.getTime() + lineDetail.durationInMinutes * 60_000); | |||
| // Handle startTime format - it can be string or number array | |||
| let start: Date; | |||
| if (Array.isArray(lineDetail.startTime)) { | |||
| console.log("Line start time is an array:", lineDetail.startTime); | |||
| // If it's an array like [2025, 12, 15, 10, 30, 0], convert to Date | |||
| const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; | |||
| start = new Date(year, month - 1, day, hour, minute, second); | |||
| } else { | |||
| start = new Date(lineDetail.startTime); | |||
| console.log("Line start time is a string:", lineDetail.startTime); | |||
| } | |||
| // Check if date is valid | |||
| if (isNaN(start.getTime())) { | |||
| console.error("Invalid startTime:", lineDetail.startTime); | |||
| setRemainingTime(null); | |||
| setIsOverTime(false); | |||
| return; | |||
| } | |||
| const durationMs = lineDetail.durationInMinutes * 60_000; | |||
| // Check if line is paused | |||
| const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused"; | |||
| // ✅ 问题2:修复 stopTime 类型处理,像 startTime 一样处理 | |||
| const parseStopTime = (stopTime: string | number[] | undefined): Date | null => { | |||
| if (!stopTime) return null; | |||
| if (Array.isArray(stopTime)) { | |||
| const [year, month, day, hour = 0, minute = 0, second = 0] = stopTime; | |||
| return new Date(year, month - 1, day, hour, minute, second); | |||
| } else { | |||
| return new Date(stopTime); | |||
| } | |||
| }; | |||
| const update = () => { | |||
| const diff = end.getTime() - Date.now(); | |||
| if (diff <= 0) { | |||
| setRemainingTime("00:00"); | |||
| if (isPaused) { | |||
| // If paused, freeze the time at the last calculated value | |||
| // If we don't have a frozen value yet, calculate it based on stopTime | |||
| if (!frozenRemainingTime) { | |||
| // ✅ 修复问题2:正确处理 stopTime 的类型(string | number[]) | |||
| const pauseTime = lineDetail.stopTime | |||
| ? parseStopTime(lineDetail.stopTime) | |||
| : null; | |||
| // 如果没有 stopTime,使用当前时间(首次暂停时) | |||
| const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime()) | |||
| ? pauseTime | |||
| : new Date(); | |||
| // ✅ 计算总暂停时间(所有已恢复的暂停记录) | |||
| const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | |||
| console.log("⏸️ Paused - calculating frozen time:", { | |||
| stopTime: lineDetail.stopTime, | |||
| pauseTime: pauseTimeToUse, | |||
| startTime: start, | |||
| totalPausedTimeMs: totalPausedTimeMs, | |||
| }); | |||
| // ✅ 实际工作时间 = 暂停时间 - 开始时间 - 已恢复的暂停时间 | |||
| const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs; | |||
| const remaining = durationMs - elapsed; | |||
| if (remaining <= 0) { | |||
| const overTime = Math.abs(remaining); | |||
| const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); | |||
| const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0"); | |||
| const frozenValue = `-${minutes}:${seconds}`; | |||
| setFrozenRemainingTime(frozenValue); | |||
| setRemainingTime(frozenValue); | |||
| setIsOverTime(true); | |||
| console.log("⏸️ Frozen time (overtime):", frozenValue); | |||
| } else { | |||
| const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0"); | |||
| const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0"); | |||
| const frozenValue = `${minutes}:${seconds}`; | |||
| setFrozenRemainingTime(frozenValue); | |||
| setRemainingTime(frozenValue); | |||
| setIsOverTime(false); | |||
| console.log("⏸️ Frozen time:", frozenValue); | |||
| } | |||
| } else { | |||
| // ✅ 关键修复:暂停时始终使用冻结的值,不重新计算 | |||
| setRemainingTime(frozenRemainingTime); | |||
| console.log("⏸️ Using frozen time:", frozenRemainingTime); | |||
| } | |||
| return; | |||
| } | |||
| const minutes = Math.floor(diff / 60000).toString().padStart(2, "0"); | |||
| const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, "0"); | |||
| setRemainingTime(`${minutes}:${seconds}`); | |||
| // If resumed or in progress, clear frozen time and continue counting | |||
| if (frozenRemainingTime && !isPaused) { | |||
| console.log("▶️ Resumed - clearing frozen time"); | |||
| setFrozenRemainingTime(null); | |||
| setLastPauseTime(null); | |||
| } | |||
| // ✅ 关键修复:计算剩余时间时,需要减去所有已恢复的暂停时间 | |||
| const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | |||
| const now = new Date(); | |||
| // ✅ 实际工作时间 = 当前时间 - 开始时间 - 所有已恢复的暂停时间 | |||
| const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs; | |||
| const remaining = durationMs - elapsed; | |||
| console.log("⏱️ Time calculation:", { | |||
| now: now, | |||
| start: start, | |||
| totalPausedTimeMs: totalPausedTimeMs, | |||
| elapsed: elapsed, | |||
| remaining: remaining, | |||
| durationMs: durationMs, | |||
| }); | |||
| if (remaining <= 0) { | |||
| // Over time - show negative time in red | |||
| const overTime = Math.abs(remaining); | |||
| const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); | |||
| const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0"); | |||
| setRemainingTime(`-${minutes}:${seconds}`); | |||
| setIsOverTime(true); | |||
| } else { | |||
| const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0"); | |||
| const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0"); | |||
| setRemainingTime(`${minutes}:${seconds}`); | |||
| setIsOverTime(false); | |||
| } | |||
| }; | |||
| update(); | |||
| const timer = setInterval(update, 1000); | |||
| return () => clearInterval(timer); | |||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); | |||
| // Only set interval if not paused | |||
| if (!isPaused) { | |||
| const timer = setInterval(update, 1000); | |||
| return () => clearInterval(timer); | |||
| } | |||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]); | |||
| // Reset frozen time when status changes from paused to in progress | |||
| useEffect(() => { | |||
| const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused"; | |||
| const isNowInProgress = lineDetail?.status === "InProgress"; | |||
| if (wasPaused && isNowInProgress && frozenRemainingTime) { | |||
| // When resuming, we need to account for the pause duration | |||
| // For now, we'll continue from the frozen time | |||
| // In a more accurate implementation, you'd fetch the issue details to get exact pause duration | |||
| setFrozenRemainingTime(null); | |||
| } | |||
| }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]); | |||
| const handleSubmitOutput = async () => { | |||
| if (!lineDetail?.id) return; | |||
| @@ -164,6 +342,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| fetchProductProcessLineDetail(lineDetail.id) | |||
| .then((detail) => { | |||
| console.log("Line Detail loaded:", { | |||
| id: detail.id, | |||
| status: detail.status, | |||
| startTime: detail.startTime, | |||
| durationInMinutes: detail.durationInMinutes, | |||
| productProcessIssueStatus: detail.productProcessIssueStatus | |||
| }); | |||
| setLineDetail(detail as any); | |||
| // 初始化 outputData 从 lineDetail | |||
| setOutputData(prev => ({ | |||
| @@ -249,6 +434,37 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| alert(t("Failed to pause. Please try again.")); | |||
| } | |||
| }; | |||
| // ✅ Add this new handler for resume | |||
| const handleResume = async () => { | |||
| if (!lineDetail?.productProcessIssueId) { | |||
| console.error("No productProcessIssueId found"); | |||
| return; | |||
| } | |||
| try { | |||
| await saveProductProcessResumeTime(lineDetail.productProcessIssueId); | |||
| console.log("✅ Resume API called successfully"); | |||
| // ✅ Refresh line detail after resume | |||
| if (lineDetail?.id) { | |||
| fetchProductProcessLineDetail(lineDetail.id) | |||
| .then((detail) => { | |||
| console.log("✅ Line detail refreshed after resume:", detail); | |||
| setLineDetail(detail as any); | |||
| // Clear frozen time when resuming | |||
| setFrozenRemainingTime(null); | |||
| setLastPauseTime(null); | |||
| }) | |||
| .catch(err => { | |||
| console.error("❌ Failed to load line detail after resume", err); | |||
| }); | |||
| } | |||
| } catch (error) { | |||
| console.error("❌ Error resuming:", error); | |||
| alert(t("Failed to resume. Please try again.")); | |||
| } | |||
| }; | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| @@ -256,13 +472,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* 如果已完成,显示合并的视图 */} | |||
| {isCompleted ? ( | |||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | |||
| <CardContent> | |||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | |||
| {t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||
| {t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| {/*<Divider sx={{ my: 2 }} />*/} | |||
| @@ -272,27 +488,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| {t("Step Information")} | |||
| </Typography> | |||
| <Grid container spacing={2} sx={{ mb: 3 }}> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Description")}:</strong> {lineDetail?.description || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Equipment")}:</strong> {equipmentName} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Status")}:</strong> {lineDetail?.status || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}> | |||
| <strong>{t("Description")}:</strong> {lineDetail?.description || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}> | |||
| <strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}> | |||
| <strong>{t("Equipment")}:</strong> {equipmentName} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}> | |||
| <strong>{t("Status")}:</strong> {t(lineDetail?.status || "-")} | |||
| </Typography> | |||
| </Grid> | |||
| </Grid> | |||
| {/*<Divider sx={{ my: 2 }} />*/} | |||
| @@ -415,7 +631,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||
| {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||
| {t("Executing")}: {lineDetail?.name} ({t("Seq")}:{lineDetail?.seqNo}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {lineDetail?.description} | |||
| @@ -426,7 +642,25 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {equipmentName} | |||
| </Typography> | |||
| {!isCompleted && remainingTime !== null && ( | |||
| <Box sx={{ mt: 2, mb: 2, p: 2, bgcolor: isOverTime ? 'error.50' : 'info.50', borderRadius: 1, border: '1px solid', borderColor: isOverTime ? 'error.main' : 'info.main' }}> | |||
| <Typography variant="body2" color="text.secondary" gutterBottom> | |||
| {t("Time Remaining")} | |||
| </Typography> | |||
| <Typography | |||
| variant="h5" | |||
| fontWeight="bold" | |||
| color={isOverTime ? 'error.main' : 'info.main'} | |||
| > | |||
| {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} | |||
| </Typography> | |||
| {lineDetail?.status === "Paused" && ( | |||
| <Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}> | |||
| {t("Timer Paused")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| )} | |||
| <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | |||
| {/* | |||
| <Button | |||
| @@ -453,7 +687,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| variant="contained" | |||
| color="success" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)} | |||
| onClick={handleResume} // ✅ Change from inline call to handler | |||
| > | |||
| {t("Continue")} | |||
| </Button> | |||
| @@ -462,6 +696,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Button | |||
| sx={{ mt: 2, alignSelf: "flex-end" }} | |||
| variant="outlined" | |||
| disabled={lineDetail?.status === 'Paused'} | |||
| onClick={() => setShowOutputTable(true)} | |||
| > | |||
| {t("Order Complete")} | |||
| @@ -521,39 +756,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* byproduct */} | |||
| {/* | |||
| <TableRow> | |||
| <TableCell> | |||
| <Stack> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| */} | |||
| {/* defect 1 */} | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| @@ -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) => ( | |||
| <span key={index}> {line} <br/></span> | |||
| ))} | |||
| </> | |||
| @@ -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<Props> = ({ | |||
| 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<Props> = ({ | |||
| { | |||
| ...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<Props> = ({ | |||
| 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<Props> = ({ | |||
| // 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<Props> = ({ | |||
| // 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 ( | |||
| <> | |||
| @@ -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<Props> = ({ | |||
| console.log(errors); | |||
| }, [errors]); | |||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||
| const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false); | |||
| const productionDate = watch("productionDate"); | |||
| const expiryDate = watch("expiryDate"); | |||
| const uom = watch("uom"); | |||
| @@ -140,7 +148,23 @@ const FgStockInForm: React.FC<Props> = ({ | |||
| 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 ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| {/* <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| @@ -250,6 +274,44 @@ const FgStockInForm: React.FC<Props> = ({ | |||
| />) | |||
| } | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| control={control} | |||
| name="productionDate" | |||
| render={({ field }) => { | |||
| return ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={textfieldSx} | |||
| label={t("productionDate")} | |||
| value={productionDate ? dayjs(productionDate) : undefined} | |||
| format={OUTPUT_DATE_FORMAT} | |||
| disabled={disabled} | |||
| onChange={(date) => { | |||
| if (!date) return; | |||
| setValue( | |||
| "productionDate", | |||
| date.format(INPUT_DATE_FORMAT), | |||
| ); | |||
| }} | |||
| inputRef={field.ref} | |||
| slotProps={{ | |||
| textField: { | |||
| error: Boolean(errors.productionDate?.message), | |||
| helperText: errors.productionDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| {/* {putawayMode || (<> | |||
| {/* {putawayMode || (<> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| @@ -313,28 +375,47 @@ const FgStockInForm: React.FC<Props> = ({ | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={textfieldSx} | |||
| label={t("expiryDate")} | |||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||
| format={OUTPUT_DATE_FORMAT} | |||
| disabled={disabled} | |||
| onChange={(date) => { | |||
| 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, | |||
| }, | |||
| }} | |||
| /> | |||
| <DatePicker | |||
| {...field} | |||
| sx={textfieldSx} | |||
| label={t("expiryDate")} | |||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||
| format={OUTPUT_DATE_FORMAT} | |||
| disabled={disabled} | |||
| onChange={(date) => { | |||
| 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: ( | |||
| <InputAdornment position='end'> | |||
| <Button | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ fontSize: '24px' }} | |||
| onClick={handleOpenModal} | |||
| > | |||
| {t("Calculate Expiry Date")} | |||
| </Button> | |||
| </InputAdornment> | |||
| ), | |||
| }) | |||
| }, | |||
| error: Boolean(errors.expiryDate?.message), | |||
| helperText: errors.expiryDate?.message, | |||
| onClick: () => setOpenExpDatePicker(true), | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| @@ -442,6 +523,13 @@ const FgStockInForm: React.FC<Props> = ({ | |||
| </Grid> */} | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| <CalculateExpiryDateModal | |||
| open={openModal} | |||
| onClose={handleOnModalClose} | |||
| onSubmit={handleReturnExpiryDate} | |||
| textfieldSx={textfieldSx} | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| export default FgStockInForm; | |||
| @@ -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: ": "總數量:" | |||
| } | |||
| } | |||
| @@ -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": "沒有找到資料", | |||