# Conflicts: # src/i18n/zh/common.jsonmaster
| @@ -25,10 +25,13 @@ const JoEdit: React.FC<Props> = async ({ searchParams }) => { | |||||
| try { | try { | ||||
| await fetchJoDetail(parseInt(id)) | await fetchJoDetail(parseInt(id)) | ||||
| } catch (e) { | } catch (e) { | ||||
| if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { | 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; | label: string; | ||||
| outputQty: number; | outputQty: number; | ||||
| outputQtyUom: string; | outputQtyUom: string; | ||||
| description: string; | |||||
| } | } | ||||
| export const preloadBomCombo = (() => { | export const preloadBomCombo = (() => { | ||||
| @@ -173,9 +173,9 @@ export const fetchDoRecordByPage = cache(async (data?: SearchDeliveryOrderInfoRe | |||||
| return response | return response | ||||
| }) | }) | ||||
| export const fetchTicketReleaseTable = cache(async ()=> { | |||||
| export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: string)=> { | |||||
| return await serverFetchJson<getTicketReleaseTable[]>( | return await serverFetchJson<getTicketReleaseTable[]>( | ||||
| `${BASE_API_URL}/doPickOrder/ticket-release-table`, | |||||
| `${BASE_API_URL}/doPickOrder/ticket-release-table/${startDate}&${endDate}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| } | } | ||||
| @@ -1,11 +1,12 @@ | |||||
| "use server"; | "use server"; | ||||
| import { cache } from 'react'; | 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 { JobOrder, JoStatus, Machine, Operator } from "."; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | ||||
| import { FileResponse } from "@/app/api/pdf/actions"; | |||||
| export interface SaveJo { | export interface SaveJo { | ||||
| bomId: number; | bomId: number; | ||||
| @@ -155,7 +156,7 @@ export const printFGStockInLabel = cache(async(data: PrintFGStockInLabelRequest) | |||||
| } | } | ||||
| return serverFetchWithNoContent( | return serverFetchWithNoContent( | ||||
| `${BASE_API_URL}/jo/print-FGPickRecordLabel?${params.toString()}`, | |||||
| `${BASE_API_URL}/jo/print-FGStockInLabel?${params.toString()}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| next: { | next: { | ||||
| @@ -214,6 +215,7 @@ export interface ProductProcessLineResponse { | |||||
| seqNo: number, | seqNo: number, | ||||
| name: string, | name: string, | ||||
| description: string, | description: string, | ||||
| equipmentDetailId: number, | |||||
| equipment_name: string, | equipment_name: string, | ||||
| equipmentDetailCode: string, | equipmentDetailCode: string, | ||||
| status: string, | status: string, | ||||
| @@ -259,6 +261,7 @@ export interface ProductProcessWithLinesResponse { | |||||
| outputQtyUom: string; | outputQtyUom: string; | ||||
| productionPriority: number; | productionPriority: number; | ||||
| jobOrderLines: JobOrderLineInfo[]; | jobOrderLines: JobOrderLineInfo[]; | ||||
| productProcessLines: ProductProcessLineResponse[]; | productProcessLines: ProductProcessLineResponse[]; | ||||
| } | } | ||||
| @@ -320,9 +323,12 @@ export interface AllJoborderProductProcessInfoResponse { | |||||
| bomId?: number; | bomId?: number; | ||||
| assignedTo: number; | assignedTo: number; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| pickOrderStatus: string; | |||||
| itemCode: string; | |||||
| itemName: string; | itemName: string; | ||||
| requiredQty: number; | requiredQty: number; | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| uom: string; | |||||
| stockInLineId: number; | stockInLineId: number; | ||||
| jobOrderCode: string; | jobOrderCode: string; | ||||
| productProcessLineCount: number; | productProcessLineCount: number; | ||||
| @@ -346,6 +352,11 @@ export interface ProductProcessLineQrscanUpadteRequest { | |||||
| equipmentTypeSubTypeEquipmentNo?: string; | equipmentTypeSubTypeEquipmentNo?: string; | ||||
| staffNo?: string; | staffNo?: string; | ||||
| } | } | ||||
| export interface NewProductProcessLineQrscanUpadteRequest{ | |||||
| productProcessLineId: number; | |||||
| equipmentCode?: string; | |||||
| staffNo?: string; | |||||
| } | |||||
| export interface ProductProcessLineDetailResponse { | export interface ProductProcessLineDetailResponse { | ||||
| id: number, | id: number, | ||||
| @@ -398,7 +409,9 @@ export interface JobOrderProcessLineDetailResponse { | |||||
| description: string; | description: string; | ||||
| equipmentId: number; | equipmentId: number; | ||||
| startTime: string | number[]; // API 返回的是数组格式 | startTime: string | number[]; // API 返回的是数组格式 | ||||
| endTime: string | number[]; // API 返回的是数组格式 | |||||
| endTime: string | number[]; | |||||
| stopTime: string | number[]; | |||||
| totalPausedTimeMs?: number; // API 返回的是数组格式 | |||||
| status: string; | status: string; | ||||
| outputFromProcessQty: number; | outputFromProcessQty: number; | ||||
| outputFromProcessUom: string; | outputFromProcessUom: string; | ||||
| @@ -524,6 +537,7 @@ export interface PickOrderLineWithLotsResponse { | |||||
| uomCode: string | null; | uomCode: string | null; | ||||
| uomDesc: string | null; | uomDesc: string | null; | ||||
| status: string | null; | status: string | null; | ||||
| handler: string | null; | |||||
| lots: LotDetailResponse[]; | lots: LotDetailResponse[]; | ||||
| } | } | ||||
| @@ -554,7 +568,16 @@ export interface LotDetailResponse { | |||||
| matchQty?: number | null; | 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) => { | export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, | `${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 () => { | export const fetchAllJoborderProductProcessInfo = cache(async () => { | ||||
| return serverFetchJson<AllJoborderProductProcessInfoResponse[]>( | return serverFetchJson<AllJoborderProductProcessInfoResponse[]>( | ||||
| `${BASE_API_URL}/product-process/Demo/Process/all`, | `${BASE_API_URL}/product-process/Demo/Process/all`, | ||||
| @@ -868,16 +903,24 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => | |||||
| ); | ); | ||||
| }); | }); | ||||
| // 获取已完成的 Job Order pick orders | // 获取已完成的 Job Order pick orders | ||||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => { | |||||
| export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { | |||||
| return serverFetchJson<any>( | 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", | method: "GET", | ||||
| next: { tags: ["jo-completed"] }, | 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 | // 获取已完成的 Job Order pick order records | ||||
| export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { | export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { | ||||
| return serverFetchJson<any[]>( | 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; | 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; | demandQty: number; | ||||
| } | } | ||||
| export interface ReleaseProdScheduleReq { | |||||
| id: number; | |||||
| } | |||||
| export interface ReleaseProdScheduleResponse { | export interface ReleaseProdScheduleResponse { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| @@ -48,6 +52,12 @@ export interface ReleaseProdScheduleResponse { | |||||
| message: string; | message: string; | ||||
| } | } | ||||
| export interface ReleaseProdScheduleRsp { | |||||
| id: number; | |||||
| code: string; | |||||
| message: string; | |||||
| } | |||||
| export interface SaveProdScheduleResponse { | export interface SaveProdScheduleResponse { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| @@ -151,6 +161,41 @@ export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInp | |||||
| return response; | 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) => { | export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { | ||||
| const response = serverFetchJson<SaveProdScheduleResponse>( | const response = serverFetchJson<SaveProdScheduleResponse>( | ||||
| `${BASE_API_URL}/productionSchedule/detail/detailed/save`, | `${BASE_API_URL}/productionSchedule/detail/detailed/save`, | ||||
| @@ -100,6 +100,13 @@ export interface DetailedProdScheduleLineResult { | |||||
| priority: number; | priority: number; | ||||
| approved: boolean; | approved: boolean; | ||||
| proportion: number; | 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 { | export interface DetailedProdScheduleLineBomMaterialResult { | ||||
| @@ -128,6 +128,7 @@ export interface StockInLine { | |||||
| dnNo?: string; | dnNo?: string; | ||||
| dnDate?: number[]; | dnDate?: number[]; | ||||
| stockQty?: number; | stockQty?: number; | ||||
| bomDescription?: string; | |||||
| handlerId?: number; | handlerId?: number; | ||||
| putAwayLines?: PutAwayLine[]; | putAwayLines?: PutAwayLine[]; | ||||
| qcResult?: QcResult[]; | qcResult?: QcResult[]; | ||||
| @@ -12,6 +12,7 @@ import { | |||||
| SearchProdSchedule, | SearchProdSchedule, | ||||
| fetchDetailedProdSchedules, | fetchDetailedProdSchedules, | ||||
| fetchProdSchedules, | fetchProdSchedules, | ||||
| exportProdSchedule, | |||||
| testDetailedSchedule, | testDetailedSchedule, | ||||
| } from "@/app/api/scheduling/actions"; | } from "@/app/api/scheduling/actions"; | ||||
| import { defaultPagingController } from "../SearchResults/SearchResults"; | import { defaultPagingController } from "../SearchResults/SearchResults"; | ||||
| @@ -21,6 +22,7 @@ import { orderBy, uniqBy, upperFirst } from "lodash"; | |||||
| import { Button, Stack } from "@mui/material"; | import { Button, Stack } from "@mui/material"; | ||||
| import isToday from 'dayjs/plugin/isToday'; | import isToday from 'dayjs/plugin/isToday'; | ||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | import useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import { FileDownload, CalendarMonth } from "@mui/icons-material"; | |||||
| dayjs.extend(isToday); | dayjs.extend(isToday); | ||||
| // may need move to "index" or "actions" | // may need move to "index" or "actions" | ||||
| @@ -77,17 +79,17 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| // type: "dateRange", | // type: "dateRange", | ||||
| // }, | // }, | ||||
| { label: t("Production Date"), paramName: "scheduleAt", type: "date" }, | { 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; | return searchCriteria; | ||||
| }, [t]); | }, [t]); | ||||
| @@ -177,18 +179,18 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| ) as ScheduleType[]; | ) as ScheduleType[]; | ||||
| const params: SearchProdSchedule = { | 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, | types: convertedTypes, | ||||
| pageNum: pagingController.pageNum - 1, | pageNum: pagingController.pageNum - 1, | ||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| @@ -207,7 +209,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| setFilteredSchedules((fs) => | setFilteredSchedules((fs) => | ||||
| orderBy( | orderBy( | ||||
| uniqBy([...fs, ...response.records], "id"), | uniqBy([...fs, ...response.records], "id"), | ||||
| ["id"], ["desc"])); | |||||
| ["id"], ["asc"])); | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -298,20 +300,67 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||||
| } | } | ||||
| }, [inputs]) | }, [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 ( | return ( | ||||
| <> | <> | ||||
| <Stack | <Stack | ||||
| direction="row" | direction="row" | ||||
| justifyContent="flex-end" | justifyContent="flex-end" | ||||
| flexWrap="wrap" | flexWrap="wrap" | ||||
| rowGap={2} | |||||
| spacing={2} // This provides consistent space between buttons | |||||
| sx={{ mb: 3 }} // Adds some margin below the button group | |||||
| > | > | ||||
| <Button | <Button | ||||
| variant="contained" | |||||
| variant="outlined" // Outlined variant makes it look distinct from the primary action | |||||
| color="primary" | |||||
| startIcon={<CalendarMonth />} | |||||
| onClick={testDetailedScheduleClick} | 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> | </Button> | ||||
| </Stack> | </Stack> | ||||
| <SearchBox | <SearchBox | ||||
| @@ -28,7 +28,9 @@ import ViewByFGDetails, { | |||||
| // FGRecord, | // FGRecord, | ||||
| } from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | } from "@/components/DetailedScheduleDetail/ViewByFGDetails"; | ||||
| import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; | 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 useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| @@ -58,7 +60,8 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| // console.log(type) | // console.log(type) | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| const params = useSearchParams(); | 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 [serverError, setServerError] = useState(""); | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const { t } = useTranslation("schedule"); | const { t } = useTranslation("schedule"); | ||||
| @@ -72,6 +75,14 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| }); | }); | ||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| const { reset, handleSubmit, setValue, getValues } = formProps | |||||
| useEffect(() => { | |||||
| if (defaultValues) { | |||||
| reset(defaultValues); | |||||
| } | |||||
| }, [defaultValues, reset]); | |||||
| const lineFormProps = useFieldArray<DetailedProdScheduleResult>({ | const lineFormProps = useFieldArray<DetailedProdScheduleResult>({ | ||||
| control: formProps.control, | control: formProps.control, | ||||
| name: "prodScheduleLines" | name: "prodScheduleLines" | ||||
| @@ -138,32 +149,64 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| }) | }) | ||||
| if (response) { | 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)) | formProps.setValue(`prodScheduleLines`, response.entity.prodScheduleLines.sort((a, b) => b.priority - a.priority)) | ||||
| // console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`)) | |||||
| } | } | ||||
| setIsUploading(false) | setIsUploading(false) | ||||
| } catch (e) { | } catch (e) { | ||||
| console.log(e) | console.log(e) | ||||
| setIsUploading(false) | 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 [tempValue, setTempValue] = useState<string | number | null>(null) | ||||
| const onEditClick = useCallback((rowId: number) => { | const onEditClick = useCallback((rowId: number) => { | ||||
| const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) | const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) | ||||
| if (row) { | if (row) { | ||||
| setTempValue(row.demandQty) | setTempValue(row.demandQty) | ||||
| } | } | ||||
| }, []) | |||||
| }, [formProps]) | |||||
| const handleEditChange = useCallback((rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => { | const handleEditChange = useCallback((rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => { | ||||
| const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) | const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) | ||||
| formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(newValue)) | formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(newValue)) | ||||
| }, []) | |||||
| }, [formProps]) | |||||
| const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => { | const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => { | ||||
| setIsUploading(true) | setIsUploading(true) | ||||
| @@ -175,6 +218,7 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| if (response) { | if (response) { | ||||
| const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) | 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) | formProps.setValue(`prodScheduleLines.${index}.bomMaterials`, response.entity.bomMaterials) | ||||
| } | } | ||||
| setIsUploading(false) | setIsUploading(false) | ||||
| @@ -182,14 +226,15 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| console.log(e) | console.log(e) | ||||
| setIsUploading(false) | setIsUploading(false) | ||||
| } | } | ||||
| }, []) | |||||
| }, [formProps, setIsUploading]) | |||||
| const onCancelClick = useCallback(async (rowId: number) => { | 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) | const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) | ||||
| formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue)) | formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue)) | ||||
| // } | |||||
| }, [tempValue]) | |||||
| } | |||||
| }, [formProps, tempValue]) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -200,9 +245,9 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | ||||
| > | > | ||||
| {/*<Grid>*/} | {/*<Grid>*/} | ||||
| {/* <Typography mb={2} variant="h4">*/} | |||||
| {/* {t(`${mode} ${title}`)}*/} | |||||
| {/* </Typography>*/} | |||||
| {/* <Typography mb={2} variant="h4">*/} | |||||
| {/* {t(`${mode} ${title}`)}*/} | |||||
| {/* </Typography>*/} | |||||
| {/*</Grid>*/} | {/*</Grid>*/} | ||||
| <DetailInfoCard | <DetailInfoCard | ||||
| // recordDetails={formProps.formState.defaultValues} | // recordDetails={formProps.formState.defaultValues} | ||||
| @@ -210,26 +255,23 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| isEditing={false} | isEditing={false} | ||||
| /> | /> | ||||
| {/* <Stack | {/* <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"> | {/* <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 && ( | {serverError && ( | ||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | <Typography variant="body2" color="error" alignSelf="flex-end"> | ||||
| {serverError} | {serverError} | ||||
| @@ -247,12 +289,24 @@ const DetailedScheduleDetailView: React.FC<Props> = ({ | |||||
| type={type} /> | type={type} /> | ||||
| {/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | {/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */} | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <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 | {/* <Button | ||||
| name="submit" | name="submit" | ||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Check />} | startIcon={<Check />} | ||||
| type="submit" | type="submit" | ||||
| // disabled={submitDisabled} | |||||
| // disabled={submitDisabled} | |||||
| > | > | ||||
| {isEditMode ? t("Save") : t("Confirm")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
| </Button> */} | </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, | id, | ||||
| type, | 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 ( | return ( | ||||
| <DetailedScheduleDetailView | <DetailedScheduleDetailView | ||||
| isEditMode={Boolean(id)} | isEditMode={Boolean(id)} | ||||
| defaultValues={prodSchedule} | defaultValues={prodSchedule} | ||||
| type={type} | type={type} | ||||
| // qcChecks={qcChecks || []} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| DetailedScheduleDetailWrapper.Loading = GeneralLoading; | DetailedScheduleDetailWrapper.Loading = GeneralLoading; | ||||
| export default DetailedScheduleDetailWrapper; | |||||
| export default DetailedScheduleDetailWrapper; | |||||
| @@ -30,16 +30,16 @@ type Props = { | |||||
| onCancelClick: (rowId: number) => void; | 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 { | const { | ||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| @@ -47,83 +47,20 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||||
| const { | const { | ||||
| getValues, | getValues, | ||||
| watch, | |||||
| formState: { errors, defaultValues, touchedFields }, | formState: { errors, defaultValues, touchedFields }, | ||||
| } = useFormContext<DetailedProdScheduleResult>(); | } = 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>[]>( | const columns = useMemo<Column<DetailedProdScheduleLineResult>[]>( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| field: "jobNo", | field: "jobNo", | ||||
| label: t("Job No."), | label: t("Job No."), | ||||
| type: "read-only", | type: "read-only", | ||||
| // editable: true, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "code", | field: "code", | ||||
| label: t("code"), | label: t("code"), | ||||
| type: "read-only", | type: "read-only", | ||||
| // editable: true, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "name", | field: "name", | ||||
| @@ -134,109 +71,75 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||||
| field: "type", | field: "type", | ||||
| label: t("type"), | label: t("type"), | ||||
| type: "read-only", | 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", | field: "demandQty", | ||||
| label: t("Demand Qty"), | label: t("Demand Qty"), | ||||
| type: "input-number", | 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", | field: "uomName", | ||||
| label: t("UoM"), | label: t("UoM"), | ||||
| type: "read-only", | 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", | 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", | field: "priority", | ||||
| label: t("Production Priority"), | label: t("Production Priority"), | ||||
| type: "read-only", | type: "read-only", | ||||
| style: { | |||||
| textAlign: "right", | |||||
| // width: "100px", | |||||
| }, | |||||
| // editable: true, | |||||
| style: { textAlign: "right" } as any, | |||||
| }, | }, | ||||
| ], | ], | ||||
| [], | |||||
| [t] | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Grid container spacing={2}> | <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}> | <Grid item xs={12}> | ||||
| {/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {`${t("FG Demand Date")}: ${date}`} | |||||
| </Typography> */} | |||||
| <ScheduleTable<DetailedProdScheduleLineResult> | <ScheduleTable<DetailedProdScheduleLineResult> | ||||
| type={type} | 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} | columns={columns} | ||||
| // setPagingController={updatePagingController} | |||||
| // pagingController={pagingController[index]} | |||||
| isAutoPaging={false} | isAutoPaging={false} | ||||
| isEditable={true} | isEditable={true} | ||||
| isEdit={isEdit} | isEdit={isEdit} | ||||
| @@ -248,8 +151,8 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||||
| onCancelClick={onCancelClick} | onCancelClick={onCancelClick} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| {/* ))} */} | |||||
| </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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import { isFinite } from "lodash"; | 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 { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { msg } from "../Swal/CustomAlerts"; | import { msg } from "../Swal/CustomAlerts"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||||
| interface Props { | interface Props { | ||||
| open: boolean; | open: boolean; | ||||
| bomCombo: BomCombo[]; | bomCombo: BomCombo[]; | ||||
| jobTypes: JobTypeResponse[]; | |||||
| onClose: () => void; | onClose: () => void; | ||||
| onSearch: () => void; | onSearch: () => void; | ||||
| } | } | ||||
| @@ -23,6 +25,7 @@ interface Props { | |||||
| const JoCreateFormModal: React.FC<Props> = ({ | const JoCreateFormModal: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| bomCombo, | bomCombo, | ||||
| jobTypes, | |||||
| onClose, | onClose, | ||||
| onSearch, | onSearch, | ||||
| }) => { | }) => { | ||||
| @@ -30,19 +33,130 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| const formProps = useForm<SaveJo>({ | const formProps = useForm<SaveJo>({ | ||||
| mode: "onChange", | 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(() => { | const onModalClose = useCallback(() => { | ||||
| reset() | reset() | ||||
| onClose() | 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) => { | const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { | ||||
| if (value != null) { | if (value != null) { | ||||
| @@ -98,7 +212,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| display: "flex", | display: "flex", | ||||
| "flex-direction": "column", | |||||
| flexDirection: "column", | |||||
| padding: "20px", | padding: "20px", | ||||
| height: "100%", //'30rem', | height: "100%", //'30rem', | ||||
| width: "100%", | width: "100%", | ||||
| @@ -199,36 +313,42 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} sm={12} md={6}> | <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} | {...field} | ||||
| label={t("Job Type")} | label={t("Job Type")} | ||||
| value={field.value?.toString() ?? ""} | value={field.value?.toString() ?? ""} | ||||
| onChange={(event) => { | onChange={(event) => { | ||||
| const value = event.target.value; | const value = event.target.value; | ||||
| console.log("Job Type changed to:", value); | |||||
| field.onChange(value === "" ? undefined : Number(value)); | field.onChange(value === "" ? undefined : Number(value)); | ||||
| }} | }} | ||||
| > | |||||
| > | |||||
| <MenuItem value=""> | <MenuItem value=""> | ||||
| <em>{t("Please select")}</em> | <em>{t("Please select")}</em> | ||||
| </MenuItem> | </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}> | <Grid item xs={12} sm={12} md={6}> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| @@ -398,6 +398,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| <JoCreateFormModal | <JoCreateFormModal | ||||
| open={isCreateJoModalOpen} | open={isCreateJoModalOpen} | ||||
| bomCombo={bomCombo} | bomCombo={bomCombo} | ||||
| jobTypes={jobTypes} | |||||
| onClose={onCloseCreateJoModal} | onClose={onCloseCreateJoModal} | ||||
| onSearch={() => { | onSearch={() => { | ||||
| setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 | setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 | ||||
| @@ -47,7 +47,10 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchPickOrders(); | fetchPickOrders(); | ||||
| }, [fetchPickOrders]); | }, [fetchPickOrders]); | ||||
| const handleBackToList = useCallback(() => { | |||||
| setSelectedPickOrderId(undefined); | |||||
| setSelectedJobOrderId(undefined); | |||||
| }, []); | |||||
| // If a pick order is selected, show JobPickExecution detail view | // If a pick order is selected, show JobPickExecution detail view | ||||
| if (selectedPickOrderId !== undefined) { | if (selectedPickOrderId !== undefined) { | ||||
| return ( | return ( | ||||
| @@ -64,7 +67,11 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} /> | |||||
| <JobPickExecution | |||||
| filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | |||||
| //onSwitchToRecordTab={onSwitchToRecordTab} | |||||
| onBackToList={handleBackToList} // 传递新的回调 | |||||
| /> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -196,16 +196,19 @@ useEffect(() => { | |||||
| if (verifiedQty === undefined || verifiedQty < 0) { | if (verifiedQty === undefined || verifiedQty < 0) { | ||||
| newErrors.actualPickQty = t('Qty is required'); | newErrors.actualPickQty = t('Qty is required'); | ||||
| } | } | ||||
| const totalQty = verifiedQty + badItemQty + missQty; | const totalQty = verifiedQty + badItemQty + missQty; | ||||
| const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; | 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) { | if (hasAnyValue && totalQty !== requiredQty) { | ||||
| newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | ||||
| } | } | ||||
| setErrors(newErrors); | setErrors(newErrors); | ||||
| return Object.keys(newErrors).length === 0; | return Object.keys(newErrors).length === 0; | ||||
| }; | }; | ||||
| @@ -214,9 +217,10 @@ useEffect(() => { | |||||
| return; | 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 (isNormalPick) { | ||||
| if (onNormalPickSubmit) { | if (onNormalPickSubmit) { | ||||
| @@ -235,11 +239,12 @@ useEffect(() => { | |||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| // ❌ 有问题(或全部为 0)才进入 Issue 提报流程 | |||||
| if (!validateForm() || !formData.pickOrderId) { | if (!validateForm() || !formData.pickOrderId) { | ||||
| return; | return; | ||||
| } | } | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const submissionData = { | const submissionData = { | ||||
| @@ -487,7 +487,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| matchStatus: lot.matchStatus, | matchStatus: lot.matchStatus, | ||||
| routerArea: lot.routerArea, | routerArea: lot.routerArea, | ||||
| routerRoute: lot.routerRoute, | 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 handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | const scannedLots = combinedLotData.filter(lot => | ||||
| lot.matchStatus === 'scanned' | |||||
| lot.matchStatus === 'scanned'|| | |||||
| lot.stockOutLineStatus === 'completed' | |||||
| ); | ); | ||||
| if (scannedLots.length === 0) { | if (scannedLots.length === 0) { | ||||
| @@ -614,7 +616,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| if (successCount > 0) { | if (successCount > 0) { | ||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| setTimeout(() => setQrScanSuccess(false), 2000); | |||||
| setTimeout(() => { | |||||
| setQrScanSuccess(false); | |||||
| // 添加:提交成功后返回到列表 | |||||
| if (onBack) { | |||||
| onBack(); | |||||
| } | |||||
| }, 2000); | |||||
| } | } | ||||
| } catch (error: any) { | } catch (error: any) { | ||||
| @@ -634,7 +642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| } finally { | } finally { | ||||
| setIsSubmittingAll(false); | setIsSubmittingAll(false); | ||||
| } | } | ||||
| }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign]); | |||||
| }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign, onBack]); | |||||
| const scannedItemsCount = useMemo(() => { | const scannedItemsCount = useMemo(() => { | ||||
| return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; | return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; | ||||
| @@ -1112,7 +1120,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| )} | )} | ||||
| {/* Combined Lot Table */} | {/* Combined Lot Table */} | ||||
| <Box> | <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', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | ||||
| <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}> | <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}> | ||||
| {!isManualScanning ? ( | {!isManualScanning ? ( | ||||
| @@ -1166,18 +1192,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| {t("QR code verified.")} | {t("QR code verified.")} | ||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| */} | |||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Index")}</TableCell> | <TableCell>{t("Index")}</TableCell> | ||||
| <TableCell>{t("Route")}</TableCell> | <TableCell>{t("Route")}</TableCell> | ||||
| <TableCell>{t("Handler")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</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> | <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| @@ -1212,6 +1239,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| {lot.routerRoute || '-'} | {lot.routerRoute || '-'} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.handler || '-'}</TableCell> | |||||
| <TableCell>{lot.itemCode}</TableCell> | <TableCell>{lot.itemCode}</TableCell> | ||||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -1232,7 +1260,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; | return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; | ||||
| })()} | })()} | ||||
| </TableCell> | </TableCell> | ||||
| {/* | |||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| {lot.matchStatus?.toLowerCase() === 'scanned' || | {lot.matchStatus?.toLowerCase() === 'scanned' || | ||||
| lot.matchStatus?.toLowerCase() === 'completed' ? ( | lot.matchStatus?.toLowerCase() === 'completed' ? ( | ||||
| @@ -1266,7 +1294,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| */} | |||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| <Box sx={{ display: 'flex', justifyContent: 'center' }}> | <Box sx={{ display: 'flex', justifyContent: 'center' }}> | ||||
| <Stack direction="row" spacing={1} alignItems="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; | const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | ||||
| handlePickQtyChange(lotKey, submitQty); | handlePickQtyChange(lotKey, submitQty); | ||||
| handleSubmitPickQtyWithQty(lot, submitQty); | handleSubmitPickQtyWithQty(lot, submitQty); | ||||
| updateSecondQrScanStatus(lot.pickOrderLineId, lot.lotId, currentUserId || 0, submitQty); | |||||
| }} | }} | ||||
| disabled={ | disabled={ | ||||
| lot.matchStatus !== 'scanned' || | |||||
| //lot.matchStatus !== 'scanned' || | |||||
| lot.lotAvailability === 'expired' || | lot.lotAvailability === 'expired' || | ||||
| lot.lotAvailability === 'status_unavailable' || | lot.lotAvailability === 'status_unavailable' || | ||||
| lot.lotAvailability === 'rejected' | lot.lotAvailability === 'rejected' | ||||
| @@ -1291,7 +1320,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| minWidth: '70px' | minWidth: '70px' | ||||
| }} | }} | ||||
| > | > | ||||
| {t("Submit")} | |||||
| {t("Confirm")} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| @@ -15,7 +15,7 @@ import { | |||||
| import { | import { | ||||
| arrayToDayjs, | arrayToDayjs, | ||||
| } from "@/app/utils/formatUtil"; | } 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 Jodetail from "./Jodetail" | ||||
| import PickExecution from "./JobPickExecution"; | import PickExecution from "./JobPickExecution"; | ||||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | 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 [totalCount, setTotalCount] = useState<number>(); | ||||
| const [isAssigning, setIsAssigning] = useState(false); | const [isAssigning, setIsAssigning] = useState(false); | ||||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | 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>( | const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | ||||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | ||||
| ); | ); | ||||
| @@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener); | 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(() => { | useEffect(() => { | ||||
| const onAssigned = () => { | const onAssigned = () => { | ||||
| localStorage.removeItem('hideCompletedUntilNext'); | localStorage.removeItem('hideCompletedUntilNext'); | ||||
| @@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| window.addEventListener('pickOrderAssigned', onAssigned); | window.addEventListener('pickOrderAssigned', onAssigned); | ||||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | return () => window.removeEventListener('pickOrderAssigned', onAssigned); | ||||
| }, []); | }, []); | ||||
| // ... existing code ... | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const handleCompletionStatusChange = (event: CustomEvent) => { | const handleCompletionStatusChange = (event: CustomEvent) => { | ||||
| @@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| return () => { | return () => { | ||||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | ||||
| }; | }; | ||||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||||
| }, [tabIndex]); | |||||
| // 新增:处理标签页切换时的打印按钮状态重置 | // 新增:处理标签页切换时的打印按钮状态重置 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| } | } | ||||
| }, [tabIndex]); | }, [tabIndex]); | ||||
| // ... existing code ... | |||||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | ||||
| if (!currentUserId) { | if (!currentUserId) { | ||||
| console.error("Missing user id in session"); | console.error("Missing user id in session"); | ||||
| @@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| return ( | return ( | ||||
| <Box sx={{ | <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={{ | <Box sx={{ | ||||
| borderBottom: '1px solid #e0e0e0' | borderBottom: '1px solid #e0e0e0' | ||||
| }}> | }}> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <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> | </Tabs> | ||||
| </Box> | </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 === 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> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; | |||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| selectedPrinter?: PrinterCombo | null; | |||||
| printQty?: number; | |||||
| } | } | ||||
| // 修改:已完成的 Job Order Pick Order 接口 | // 修改:已完成的 Job Order Pick Order 接口 | ||||
| @@ -99,9 +101,15 @@ interface LotDetail { | |||||
| itemName: string; | itemName: string; | ||||
| uomCode: string; | uomCode: string; | ||||
| uomDesc: 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 { t } = useTranslation("jo"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | 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 [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | 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({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -157,7 +151,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| try { | try { | ||||
| console.log("🔍 Fetching completed Job Order pick orders (pick completed only)..."); | 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 | // Fix: Ensure the data is always an array | ||||
| const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | ||||
| @@ -226,7 +220,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| setFilteredJobOrderPickOrders(filtered); | setFilteredJobOrderPickOrders(filtered); | ||||
| console.log("Filtered Job Order pick orders count:", filtered.length); | console.log("Filtered Job Order pick orders count:", filtered.length); | ||||
| }, [completedJobOrderPickOrders]); | }, [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(() => { | const handleSearchReset = useCallback(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| @@ -433,18 +439,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} | ||||
| </Typography> | </Typography> | ||||
| </Stack> | </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> | </CardContent> | ||||
| </Card> | </Card> | ||||
| @@ -545,12 +539,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| height: '100%' | height: '100%' | ||||
| }}> | }}> | ||||
| <Checkbox | <Checkbox | ||||
| checked={lot.secondQrScanStatus === 'completed'} | |||||
| checked={lot.match_status === 'completed'} | |||||
| disabled={true} | disabled={true} | ||||
| readOnly={true} | readOnly={true} | ||||
| size="large" | size="large" | ||||
| sx={{ | sx={{ | ||||
| color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400', | |||||
| color: lot.match_status === 'completed' ? 'success.main' : 'grey.400', | |||||
| '&.Mui-checked': { | '&.Mui-checked': { | ||||
| color: 'success.main', | color: 'success.main', | ||||
| }, | }, | ||||
| @@ -600,37 +594,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} | ||||
| </Typography> | </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 ? ( | {filteredJobOrderPickOrders.length === 0 ? ( | ||||
| <Box sx={{ p: 3, textAlign: 'center' }}> | <Box sx={{ p: 3, textAlign: 'center' }}> | ||||
| @@ -652,7 +616,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => | |||||
| {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} | |||||
| {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} | ||||
| @@ -42,8 +42,6 @@ import { | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| // 修改:使用 Job Order API | // 修改:使用 Job Order API | ||||
| import { | import { | ||||
| //fetchJobOrderLotsHierarchical, | |||||
| //fetchUnassignedJobOrderPickOrders, | |||||
| assignJobOrderPickOrder, | assignJobOrderPickOrder, | ||||
| fetchJobOrderLotsHierarchicalByPickOrderId, | fetchJobOrderLotsHierarchicalByPickOrderId, | ||||
| updateJoPickOrderHandledBy, | updateJoPickOrderHandledBy, | ||||
| @@ -67,7 +65,8 @@ import FGPickOrderCard from "./FGPickOrderCard"; | |||||
| import LotConfirmationModal from "./LotConfirmationModal"; | import LotConfirmationModal from "./LotConfirmationModal"; | ||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| onSwitchToRecordTab: () => void; | |||||
| //onSwitchToRecordTab: () => void; | |||||
| onBackToList?: () => void; | |||||
| } | } | ||||
| // QR Code Modal Component (from GoodPickExecution) | // 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 { t } = useTranslation("jo"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -412,6 +411,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| pickOrderType: data.pickOrder.type, | pickOrderType: data.pickOrder.type, | ||||
| pickOrderStatus: data.pickOrder.status, | pickOrderStatus: data.pickOrder.status, | ||||
| pickOrderAssignTo: data.pickOrder.assignTo, | pickOrderAssignTo: data.pickOrder.assignTo, | ||||
| handler: line.handler, | |||||
| }); | }); | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -537,6 +537,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setCombinedDataLoading(false); | setCombinedDataLoading(false); | ||||
| } | } | ||||
| }, [getAllLotsFromHierarchical]); | }, [getAllLotsFromHierarchical]); | ||||
| const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | ||||
| if (!currentUserId || !pickOrderId || !itemId) { | if (!currentUserId || !pickOrderId || !itemId) { | ||||
| return; | return; | ||||
| @@ -901,11 +902,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| // Use the first active suggested lot as the "expected" lot | // Use the first active suggested lot as the "expected" lot | ||||
| const expectedLot = activeSuggestedLots[0]; | const expectedLot = activeSuggestedLots[0]; | ||||
| // 2) Check if the scanned lot matches exactly | |||||
| if (scanned?.lotNo === expectedLot.lotNo) { | if (scanned?.lotNo === expectedLot.lotNo) { | ||||
| // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) | |||||
| console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | ||||
| if (!expectedLot.stockOutLineId) { | if (!expectedLot.stockOutLineId) { | ||||
| console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| @@ -922,24 +921,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| status: "checked", | 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); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } else { | } else { | ||||
| console.warn("Unexpected response code from backend:", res.code); | |||||
| console.warn("Unexpected response from backend:", res); | |||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } | } | ||||
| @@ -949,7 +957,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } | } | ||||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||||
| return; // ✅ 直接返回,不再调用后面的分支 | |||||
| } | } | ||||
| // Case 2: Same item, different lot - show confirmation modal | // Case 2: Same item, different lot - show confirmation modal | ||||
| @@ -977,7 +985,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| return; | return; | ||||
| } | } | ||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); | |||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); | |||||
| const handleManualInputSubmit = useCallback(() => { | const handleManualInputSubmit = useCallback(() => { | ||||
| @@ -1310,6 +1318,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| console.error("Error submitting pick quantity:", error); | console.error("Error submitting pick quantity:", error); | ||||
| } | } | ||||
| }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); | }, [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 handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | const scannedLots = combinedLotData.filter(lot => | ||||
| lot.stockOutLineStatus === 'checked' | lot.stockOutLineStatus === 'checked' | ||||
| @@ -1365,8 +1381,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| checkAndAutoAssignNext(); | checkAndAutoAssignNext(); | ||||
| if (onSwitchToRecordTab) { | |||||
| onSwitchToRecordTab(); | |||||
| if (onBackToList) { | |||||
| onBackToList(); | |||||
| } | } | ||||
| }, 2000); | }, 2000); | ||||
| } else { | } else { | ||||
| @@ -1380,7 +1396,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| } finally { | } finally { | ||||
| setIsSubmittingAll(false); | setIsSubmittingAll(false); | ||||
| } | } | ||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab]) | |||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList]) | |||||
| // Calculate scanned items count | // Calculate scanned items count | ||||
| const scannedItemsCount = useMemo(() => { | const scannedItemsCount = useMemo(() => { | ||||
| @@ -1544,7 +1560,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| }, [startScan]); | }, [startScan]); | ||||
| const handleStopScan = useCallback(() => { | const handleStopScan = useCallback(() => { | ||||
| console.log("⏹️ Stopping manual QR scan..."); | |||||
| console.log(" Stopping manual QR scan..."); | |||||
| setIsManualScanning(false); | setIsManualScanning(false); | ||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -1563,7 +1579,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| }, [isManualScanning, stopScan, resetScan]); | }, [isManualScanning, stopScan, resetScan]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isManualScanning && combinedLotData.length === 0) { | if (isManualScanning && combinedLotData.length === 0) { | ||||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||||
| console.log(" No data available, auto-stopping QR scan..."); | |||||
| handleStopScan(); | handleStopScan(); | ||||
| } | } | ||||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | }, [combinedLotData.length, isManualScanning, handleStopScan]); | ||||
| @@ -1677,16 +1693,59 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| </Box> | </Box> | ||||
| </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.")} | {t("QR code does not match any item in current orders.")} | ||||
| </Alert> | </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}> | <TableContainer component={Paper}> | ||||
| <Table> | <Table> | ||||
| @@ -1694,6 +1753,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Index")}</TableCell> | <TableCell>{t("Index")}</TableCell> | ||||
| <TableCell>{t("Route")}</TableCell> | <TableCell>{t("Route")}</TableCell> | ||||
| <TableCell>{t("Handler")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| @@ -1733,6 +1793,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| {lot.routerRoute || '-'} | {lot.routerRoute || '-'} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{lot.handler || '-'}</TableCell> | |||||
| <TableCell>{lot.itemCode}</TableCell> | <TableCell>{lot.itemCode}</TableCell> | ||||
| <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -1837,6 +1898,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||||
| > | > | ||||
| {t("Issue")} | {t("Issue")} | ||||
| </Button> | </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> | </Stack> | ||||
| </Box> | </Box> | ||||
| </TableCell> | </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 dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { | import { | ||||
| fetchProductProcessById, | |||||
| updateProductProcessLineQrscan, | |||||
| // updateProductProcessLineQrscan, | |||||
| newUpdateProductProcessLineQrscan, | |||||
| fetchProductProcessLineDetail, | fetchProductProcessLineDetail, | ||||
| ProductProcessLineDetailResponse, | |||||
| JobOrderProcessLineDetailResponse, | JobOrderProcessLineDetailResponse, | ||||
| updateLineOutput, | |||||
| ProductProcessLineInfoResponse, | ProductProcessLineInfoResponse, | ||||
| ProductProcessResponse, | |||||
| ProductProcessLineResponse, | |||||
| completeProductProcessLine, | |||||
| startProductProcessLine, | startProductProcessLine, | ||||
| fetchProductProcessesByJobOrderId | fetchProductProcessesByJobOrderId | ||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| @@ -61,7 +56,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| onBack, | onBack, | ||||
| fromJosave, | fromJosave, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | |||||
| const { t } = useTranslation("common"); | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | 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 [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | ||||
| const [scannedEquipmentId, setScannedEquipmentId] = 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 [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 [scanningLineId, setScanningLineId] = useState<number | null>(null); | ||||
| const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | ||||
| const [showScanDialog, setShowScanDialog] = useState(false); | const [showScanDialog, setShowScanDialog] = useState(false); | ||||
| @@ -224,7 +221,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| const currentLine = lines.find(l => l.id === lineId); | const currentLine = lines.find(l => l.id === lineId); | ||||
| if (currentLine && currentLine.equipment_name) { | if (currentLine && currentLine.equipment_name) { | ||||
| const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`; | const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`; | ||||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); | |||||
| setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); | |||||
| console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`); | console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`); | ||||
| } else { | } else { | ||||
| // 如果找不到 line,尝试从 API 获取 line detail | // 如果找不到 line,尝试从 API 获取 line detail | ||||
| @@ -232,11 +229,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| fetchProductProcessLineDetail(lineId) | fetchProductProcessLineDetail(lineId) | ||||
| .then((lineDetail) => { | .then((lineDetail) => { | ||||
| // 从 lineDetail 中获取 equipment_name | // 从 lineDetail 中获取 equipment_name | ||||
| // 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整 | |||||
| const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; | const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; | ||||
| if (equipmentName) { | if (equipmentName) { | ||||
| const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; | const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; | ||||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); | |||||
| setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); | |||||
| console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`); | console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`); | ||||
| } else { | } else { | ||||
| console.warn(`Equipment name not found in line detail for lineId: ${lineId}`); | console.warn(`Equipment name not found in line detail for lineId: ${lineId}`); | ||||
| @@ -249,7 +245,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| return; | return; | ||||
| } | } | ||||
| // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo | // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo | ||||
| // 例如:{2fitestu123} = staffNo: "123" | // 例如:{2fitestu123} = staffNo: "123" | ||||
| // 例如:{2fitestustaff001} = staffNo: "staff001" | // 例如:{2fitestustaff001} = staffNo: "staff001" | ||||
| @@ -271,11 +266,11 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| return; | 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) { | if (equipmentCodeMatch) { | ||||
| const equipmentCode = equipmentCodeMatch[1].trim(); | const equipmentCode = equipmentCodeMatch[1].trim(); | ||||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode); | |||||
| setScannedEquipmentCode(equipmentCode); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -286,11 +281,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| setScannedStaffNo(String(qrData.staffNo)); | setScannedStaffNo(String(qrData.staffNo)); | ||||
| } | } | ||||
| if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) { | if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) { | ||||
| setScannedEquipmentTypeSubTypeEquipmentNo( | |||||
| setScannedEquipmentCode( | |||||
| String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode) | String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode) | ||||
| ); | ); | ||||
| } | } | ||||
| // TODO: 处理 JSON 格式的 QR 码 | |||||
| } catch { | } catch { | ||||
| // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode | // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode | ||||
| if (trimmedValue.length > 0) { | if (trimmedValue.length > 0) { | ||||
| @@ -299,7 +293,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| setScannedStaffNo(trimmedValue); | setScannedStaffNo(trimmedValue); | ||||
| } else if (trimmedValue.includes("-")) { | } else if (trimmedValue.includes("-")) { | ||||
| // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") | // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") | ||||
| setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue); | |||||
| setScannedEquipmentCode(trimmedValue); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -323,36 +317,41 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| console.log("submitScanAndStart called with:", { | console.log("submitScanAndStart called with:", { | ||||
| lineId, | lineId, | ||||
| scannedStaffNo, | scannedStaffNo, | ||||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| // scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| scannedEquipmentCode, | |||||
| }); | }); | ||||
| if (!scannedStaffNo) { | if (!scannedStaffNo) { | ||||
| console.log("No staffNo, cannot submit"); | console.log("No staffNo, cannot submit"); | ||||
| setIsAutoSubmitting(false); | setIsAutoSubmitting(false); | ||||
| return false; // 没有 staffNo,不能提交 | |||||
| return false; | |||||
| } | } | ||||
| try { | try { | ||||
| // 获取 line detail 以检查 bomProcessEquipmentId | |||||
| const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); | 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, | productProcessLineId: lineId, | ||||
| staffNo: scannedStaffNo, | staffNo: scannedStaffNo, | ||||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| equipmentCode: effectiveEquipmentCode, | |||||
| }); | }); | ||||
| const response = await updateProductProcessLineQrscan({ | |||||
| const response = await newUpdateProductProcessLineQrscan({ | |||||
| productProcessLineId: lineId, | productProcessLineId: lineId, | ||||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined, | |||||
| staffNo: scannedStaffNo || undefined, | |||||
| equipmentCode: effectiveEquipmentCode ?? "", | |||||
| staffNo: scannedStaffNo, | |||||
| }); | }); | ||||
| console.log("Scan submit response:", response); | 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); | setIsAutoSubmitting(false); | ||||
| if (autoSubmitTimerRef.current) { | if (autoSubmitTimerRef.current) { | ||||
| clearTimeout(autoSubmitTimerRef.current); | clearTimeout(autoSubmitTimerRef.current); | ||||
| @@ -360,25 +359,31 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| } | } | ||||
| return false; | return false; | ||||
| } | } | ||||
| // 验证通过,继续执行后续步骤 | |||||
| console.log("Validation passed, starting line..."); | console.log("Validation passed, starting line..."); | ||||
| handleStopScan(); | handleStopScan(); | ||||
| setShowScanDialog(false); | setShowScanDialog(false); | ||||
| setIsAutoSubmitting(false); | setIsAutoSubmitting(false); | ||||
| await handleStartLine(lineId); | await handleStartLine(lineId); | ||||
| setSelectedLineId(lineId); | setSelectedLineId(lineId); | ||||
| setIsExecutingLine(true); | setIsExecutingLine(true); | ||||
| await fetchProcessDetail(); | await fetchProcessDetail(); | ||||
| return true; | return true; | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error submitting scan:", error); | console.error("Error submitting scan:", error); | ||||
| alert("Failed to submit scan data. Please try again."); | |||||
| setIsAutoSubmitting(false); | setIsAutoSubmitting(false); | ||||
| return false; | return false; | ||||
| } | } | ||||
| }, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]); | |||||
| }, [ | |||||
| scannedStaffNo, | |||||
| scannedEquipmentCode, | |||||
| lineDetailForScan, | |||||
| t, | |||||
| fetchProcessDetail, | |||||
| ]); | |||||
| const handleSubmitScanAndStart = useCallback(async (lineId: number) => { | const handleSubmitScanAndStart = useCallback(async (lineId: number) => { | ||||
| console.log("handleSubmitScanAndStart called with lineId:", lineId); | console.log("handleSubmitScanAndStart called with lineId:", lineId); | ||||
| @@ -408,6 +413,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| setProcessedQrCodes(new Set()); | setProcessedQrCodes(new Set()); | ||||
| setScannedOperatorId(null); | setScannedOperatorId(null); | ||||
| setScannedEquipmentId(null); | setScannedEquipmentId(null); | ||||
| setScannedStaffNo(null); // ✅ Add this | |||||
| setScannedEquipmentCode(null); | |||||
| setIsAutoSubmitting(false); // 添加:重置自动提交状态 | setIsAutoSubmitting(false); // 添加:重置自动提交状态 | ||||
| setLineDetailForScan(null); | setLineDetailForScan(null); | ||||
| // 获取 line detail 以获取 bomProcessEquipmentId | // 获取 line detail 以获取 bomProcessEquipmentId | ||||
| @@ -431,7 +438,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| } | } | ||||
| setIsManualScanning(false); | setIsManualScanning(false); | ||||
| setIsAutoSubmitting(false); // 添加:重置自动提交状态 | |||||
| setIsAutoSubmitting(false); | |||||
| setScannedStaffNo(null); // ✅ Add this | |||||
| setScannedEquipmentCode(null); | |||||
| stopScan(); | stopScan(); | ||||
| resetScan(); | resetScan(); | ||||
| }, [stopScan, resetScan]); | }, [stopScan, resetScan]); | ||||
| @@ -446,20 +455,21 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| } | } | ||||
| }; | }; | ||||
| // 提交扫描结果并验证 | // 提交扫描结果并验证 | ||||
| /* | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("Auto-submit check:", { | console.log("Auto-submit check:", { | ||||
| scanningLineId, | scanningLineId, | ||||
| scannedStaffNo, | scannedStaffNo, | ||||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||||
| scannedEquipmentCode, | |||||
| isAutoSubmitting, | isAutoSubmitting, | ||||
| isManualScanning, | isManualScanning, | ||||
| }); | }); | ||||
| // ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId | |||||
| if ( | if ( | ||||
| scanningLineId && | scanningLineId && | ||||
| scannedStaffNo !== null && | scannedStaffNo !== null && | ||||
| scannedEquipmentTypeSubTypeEquipmentNo !== null && | |||||
| (scannedEquipmentCode !== null) && | |||||
| !isAutoSubmitting && | !isAutoSubmitting && | ||||
| isManualScanning | 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(() => { | useEffect(() => { | ||||
| return () => { | return () => { | ||||
| if (autoSubmitTimerRef.current) { | if (autoSubmitTimerRef.current) { | ||||
| @@ -502,6 +513,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| setScannedEquipmentId(null); | setScannedEquipmentId(null); | ||||
| setProcessedQrCodes(new Set()); | setProcessedQrCodes(new Set()); | ||||
| setScannedStaffNo(null); | |||||
| setScannedEquipmentCode(null); | |||||
| setProcessedQrCodes(new Set()); | |||||
| // 清除之前的定时器 | // 清除之前的定时器 | ||||
| if (autoSubmitTimerRef.current) { | if (autoSubmitTimerRef.current) { | ||||
| clearTimeout(autoSubmitTimerRef.current); | clearTimeout(autoSubmitTimerRef.current); | ||||
| @@ -764,9 +778,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <Box> | <Box> | ||||
| <Typography variant="body2" color="text.secondary"> | <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> | </Typography> | ||||
| </Box> | </Box> | ||||
| @@ -792,7 +807,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | ||||
| disabled={!scannedStaffNo} | |||||
| disabled={!scannedStaffNo } | |||||
| > | > | ||||
| {t("Submit & Start")} | {t("Submit & Start")} | ||||
| </Button> | </Button> | ||||
| @@ -115,7 +115,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||||
| }, [fetchData]); | }, [fetchData]); | ||||
| // PickTable 组件内容 | // PickTable 组件内容 | ||||
| const getStockAvailable = (line: JobOrderLine) => { | const getStockAvailable = (line: JobOrderLine) => { | ||||
| if (line.type?.toLowerCase() === "consumables") { | |||||
| if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { | |||||
| return null; | return null; | ||||
| } | } | ||||
| const inventory = inventoryData.find(inv => | const inventory = inventoryData.find(inv => | ||||
| @@ -158,7 +158,7 @@ const isStockSufficient = (line: JobOrderLine) => { | |||||
| const stockCounts = useMemo(() => { | const stockCounts = useMemo(() => { | ||||
| // 过滤掉 consumables 类型的 lines | // 过滤掉 consumables 类型的 lines | ||||
| const nonConsumablesLines = jobOrderLines.filter( | 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 total = nonConsumablesLines.length; | ||||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | ||||
| @@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { | |||||
| const response = await deleteJobOrder(jobOrderId) | const response = await deleteJobOrder(jobOrderId) | ||||
| if (response) { | if (response) { | ||||
| //setProcessData(response.entity); | //setProcessData(response.entity); | ||||
| await fetchData(); | |||||
| //await fetchData(); | |||||
| onBack(); | |||||
| } | } | ||||
| }, [jobOrderId]); | }, [jobOrderId]); | ||||
| const handleRelease = useCallback(async ( jobOrderId: number) => { | const handleRelease = useCallback(async ( jobOrderId: number) => { | ||||
| @@ -315,7 +316,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| headerAlign: "left", | headerAlign: "left", | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params) => { | 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", | align: "left", | ||||
| headerAlign: "left", | headerAlign: "left", | ||||
| renderCell: (params) => { | 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 = | const productionProcessesLineRemarkTableRows = | ||||
| processData?.productProcessLines?.map((line: any) => ({ | processData?.productProcessLines?.map((line: any) => ({ | ||||
| id: line.seqNo, | id: line.seqNo, | ||||
| seqNo: line.seqNo, | seqNo: line.seqNo, | ||||
| description: line.description ?? "", | description: line.description ?? "", | ||||
| })) ?? []; | })) ?? []; | ||||
| @@ -486,21 +499,37 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| /> | /> | ||||
| </Box> | </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 ( | return ( | ||||
| @@ -94,7 +94,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| setModalInfo({ | setModalInfo({ | ||||
| id: process.stockInLineId, | id: process.stockInLineId, | ||||
| expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), | |||||
| //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), | |||||
| // 视需要补 itemId、jobOrderId 等 | // 视需要补 itemId、jobOrderId 等 | ||||
| }); | }); | ||||
| setOpenModal(true); | setOpenModal(true); | ||||
| @@ -155,9 +156,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| const closeNewModal = useCallback(() => { | const closeNewModal = useCallback(() => { | ||||
| // const response = updateJo({ id: 1, status: "storing" }); | // const response = updateJo({ id: 1, status: "storing" }); | ||||
| setOpenModal(false); // Close the modal first | setOpenModal(false); // Close the modal first | ||||
| fetchProcesses(); | |||||
| // setTimeout(() => { | // setTimeout(() => { | ||||
| // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | ||||
| }, []); | |||||
| }, [fetchProcesses]); | |||||
| const startIdx = page * PER_PAGE; | const startIdx = page * PER_PAGE; | ||||
| const paged = processes.slice(startIdx, startIdx + PER_PAGE); | const paged = processes.slice(startIdx, startIdx + PER_PAGE); | ||||
| @@ -233,10 +235,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| </Stack> | </Stack> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Item Name")}: {process.itemName} | |||||
| {t("Item Name")}: {process.itemCode} {process.itemName} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Required Qty")}: {process.requiredQty} | |||||
| {t("Required Qty")}: {process.requiredQty} {process.uom} | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} | {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 | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | 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)} | onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)} | ||||
| > | > | ||||
| {t("Matching Stock")} | {t("Matching Stock")} | ||||
| @@ -2,15 +2,19 @@ | |||||
| import React, { useState, useEffect, useCallback } from "react"; | import React, { useState, useEffect, useCallback } from "react"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; | |||||
| import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; | import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; | ||||
| import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | ||||
| import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | ||||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | ||||
| import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | |||||
| import { | import { | ||||
| fetchProductProcesses, | fetchProductProcesses, | ||||
| fetchProductProcessesByJobOrderId, | fetchProductProcessesByJobOrderId, | ||||
| ProductProcessLineResponse | ProductProcessLineResponse | ||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { useTranslation } from "react-i18next"; | |||||
| type PrinterCombo = { | type PrinterCombo = { | ||||
| id: number; | id: number; | ||||
| value: number; | value: number; | ||||
| @@ -25,17 +29,25 @@ type PrinterCombo = { | |||||
| interface ProductionProcessPageProps { | interface ProductionProcessPageProps { | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| } | } | ||||
| const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | ||||
| const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | ||||
| const { t } = useTranslation(["common"]); | |||||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | ||||
| const [selectedMatchingStock, setSelectedMatchingStock] = useState<{ | const [selectedMatchingStock, setSelectedMatchingStock] = useState<{ | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| productProcessId: number; | productProcessId: number; | ||||
| } | null>(null); | } | null>(null); | ||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | 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 恢复状态(仅在客户端) | // 从 sessionStorage 恢复状态(仅在客户端) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | 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) { | if (selectedMatchingStock) { | ||||
| return ( | return ( | ||||
| <JobPickExecutionsecondscan | <JobPickExecutionsecondscan | ||||
| @@ -84,6 +100,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| /> | /> | ||||
| ); | ); | ||||
| } | } | ||||
| if (selectedProcessId !== null) { | if (selectedProcessId !== null) { | ||||
| return ( | return ( | ||||
| <ProductionProcessJobOrderDetail | <ProductionProcessJobOrderDetail | ||||
| @@ -94,21 +111,86 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| } | } | ||||
| return ( | 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, | CardContent, | ||||
| Grid, | Grid, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Alert } from "@mui/material"; | |||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | import QrCodeIcon from '@mui/icons-material/QrCode'; | ||||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | ||||
| import StopIcon from "@mui/icons-material/Stop"; | import StopIcon from "@mui/icons-material/Stop"; | ||||
| @@ -75,6 +77,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | ||||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | 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[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | ||||
| const [pauseReason, setPauseReason] = useState(""); | const [pauseReason, setPauseReason] = useState(""); | ||||
| // 检查是否两个都已扫描 | // 检查是否两个都已扫描 | ||||
| @@ -91,7 +96,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| fetchProductProcessLineDetail(lineId) | fetchProductProcessLineDetail(lineId) | ||||
| .then((detail) => { | .then((detail) => { | ||||
| setLineDetail(detail as any); | 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 => ({ | setOutputData(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | productProcessLineId: detail.id, | ||||
| @@ -112,27 +125,192 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| }); | }); | ||||
| }, [lineId]); | }, [lineId]); | ||||
| useEffect(() => { | 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) { | 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); | setRemainingTime(null); | ||||
| setIsOverTime(false); | |||||
| return; | 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 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; | 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(); | 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 () => { | const handleSubmitOutput = async () => { | ||||
| if (!lineDetail?.id) return; | if (!lineDetail?.id) return; | ||||
| @@ -164,6 +342,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| fetchProductProcessLineDetail(lineDetail.id) | fetchProductProcessLineDetail(lineDetail.id) | ||||
| .then((detail) => { | .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); | setLineDetail(detail as any); | ||||
| // 初始化 outputData 从 lineDetail | // 初始化 outputData 从 lineDetail | ||||
| setOutputData(prev => ({ | setOutputData(prev => ({ | ||||
| @@ -249,6 +434,37 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| alert(t("Failed to pause. Please try again.")); | 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 ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| @@ -256,13 +472,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| {/* 如果已完成,显示合并的视图 */} | {/* 如果已完成,显示合并的视图 */} | ||||
| {isCompleted ? ( | {isCompleted ? ( | ||||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | ||||
| <CardContent> | <CardContent> | ||||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | <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> | </Typography> | ||||
| {/*<Divider sx={{ my: 2 }} />*/} | {/*<Divider sx={{ my: 2 }} />*/} | ||||
| @@ -272,27 +488,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| {t("Step Information")} | {t("Step Information")} | ||||
| </Typography> | </Typography> | ||||
| <Grid container spacing={2} sx={{ mb: 3 }}> | <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> | ||||
| <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 }} />*/} | {/*<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%' }}> | <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}> | ||||
| <CardContent> | <CardContent> | ||||
| <Typography variant="h6" color="primary.main" gutterBottom> | <Typography variant="h6" color="primary.main" gutterBottom> | ||||
| {t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo}) | |||||
| {t("Executing")}: {lineDetail?.name} ({t("Seq")}:{lineDetail?.seqNo}) | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {lineDetail?.description} | {lineDetail?.description} | ||||
| @@ -426,7 +642,25 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Equipment")}: {equipmentName} | {t("Equipment")}: {equipmentName} | ||||
| </Typography> | </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 }}> | <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | ||||
| {/* | {/* | ||||
| <Button | <Button | ||||
| @@ -453,7 +687,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| variant="contained" | variant="contained" | ||||
| color="success" | color="success" | ||||
| startIcon={<PlayArrowIcon />} | startIcon={<PlayArrowIcon />} | ||||
| onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)} | |||||
| onClick={handleResume} // ✅ Change from inline call to handler | |||||
| > | > | ||||
| {t("Continue")} | {t("Continue")} | ||||
| </Button> | </Button> | ||||
| @@ -462,6 +696,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <Button | <Button | ||||
| sx={{ mt: 2, alignSelf: "flex-end" }} | sx={{ mt: 2, alignSelf: "flex-end" }} | ||||
| variant="outlined" | variant="outlined" | ||||
| disabled={lineDetail?.status === 'Paused'} | |||||
| onClick={() => setShowOutputTable(true)} | onClick={() => setShowOutputTable(true)} | ||||
| > | > | ||||
| {t("Order Complete")} | {t("Order Complete")} | ||||
| @@ -521,39 +756,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </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 */} | {/* defect 1 */} | ||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -413,10 +413,11 @@ useEffect(() => { | |||||
| } else { return 60} | } else { return 60} | ||||
| }; | }; | ||||
| const formattedDesc = (content: string = "") => { | |||||
| const formattedDesc = (content: string | null | undefined = "") => { | |||||
| const safeContent = content || ""; | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {content.split("\\n").map((line, index) => ( | |||||
| {safeContent.split("\\n").map((line, index) => ( | |||||
| <span key={index}> {line} <br/></span> | <span key={index}> {line} <br/></span> | ||||
| ))} | ))} | ||||
| </> | </> | ||||
| @@ -40,7 +40,7 @@ import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForS | |||||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | ||||
| import FgStockInForm from "../StockIn/FgStockInForm"; | import FgStockInForm from "../StockIn/FgStockInForm"; | ||||
| import LoadingComponent from "../General/LoadingComponent"; | import LoadingComponent from "../General/LoadingComponent"; | ||||
| import { printFGStockInLabel, PrintFGStockInLabelRequest } from "@/app/api/jo/actions"; | |||||
| import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions"; | |||||
| const style = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| @@ -119,7 +119,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| const res = await fetchStockInLineInfo(stockInLineId); | const res = await fetchStockInLineInfo(stockInLineId); | ||||
| if (res) { | if (res) { | ||||
| console.log("%c Fetched Stock In Line: ", "color:orange", 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); | // fetchQcResultData(stockInLineId); | ||||
| } else throw("Result is undefined"); | } else throw("Result is undefined"); | ||||
| @@ -168,8 +168,8 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| { | { | ||||
| ...d, | ...d, | ||||
| // status: d.status ?? "pending", | // 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") | receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") | ||||
| : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | ||||
| acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0, | acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0, | ||||
| @@ -350,9 +350,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| const qcData = { | const qcData = { | ||||
| dnNo : data.dnNo? data.dnNo : "DN00000", | dnNo : data.dnNo? data.dnNo : "DN00000", | ||||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | // 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, | qcAccept: qcAccept? qcAccept : false, | ||||
| acceptQty: acceptQty? acceptQty : 0, | acceptQty: acceptQty? acceptQty : 0, | ||||
| @@ -396,6 +396,52 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | ||||
| // confirmButtonText: t("confirm putaway"), html: ""}); | // confirmButtonText: t("confirm putaway"), html: ""}); | ||||
| // onOpenPutaway(); | // 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"); | closeHandler({}, "backdropClick"); | ||||
| // setTabIndex(1); // Need to go Putaway tab? | // setTabIndex(1); // Need to go Putaway tab? | ||||
| } else { | } else { | ||||
| @@ -540,24 +586,38 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| // return isPassed | // return isPassed | ||||
| // }, [acceptQty, formProps]) | // }, [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); | 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 ( | return ( | ||||
| <> | <> | ||||
| @@ -12,11 +12,12 @@ import { | |||||
| TextField, | TextField, | ||||
| Tooltip, | Tooltip, | ||||
| Typography, | Typography, | ||||
| Button, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Controller, useFormContext } from "react-hook-form"; | import { Controller, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { useCallback, useEffect, useMemo } from "react"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | import { | ||||
| GridColDef, | GridColDef, | ||||
| GridRowIdGetter, | GridRowIdGetter, | ||||
| @@ -35,6 +36,11 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import dayjs from "dayjs"; | 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 | // change PurchaseQcResult to stock in entry props | ||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | itemDetail: StockInLine; | ||||
| @@ -115,6 +121,8 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| console.log(errors); | console.log(errors); | ||||
| }, [errors]); | }, [errors]); | ||||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||||
| const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false); | |||||
| const productionDate = watch("productionDate"); | const productionDate = watch("productionDate"); | ||||
| const expiryDate = watch("expiryDate"); | const expiryDate = watch("expiryDate"); | ||||
| const uom = watch("uom"); | const uom = watch("uom"); | ||||
| @@ -140,7 +148,23 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | ||||
| }, [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 container justifyContent="flex-start" alignItems="flex-start"> | ||||
| {/* <Grid item xs={12}> | {/* <Grid item xs={12}> | ||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | <Typography variant="h6" display="block" marginBlockEnd={1}> | ||||
| @@ -250,6 +274,44 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| />) | />) | ||||
| } | } | ||||
| </Grid> | </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 || (<> | {/* {putawayMode || (<> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <Controller | <Controller | ||||
| @@ -313,28 +375,47 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| dateAdapter={AdapterDayjs} | dateAdapter={AdapterDayjs} | ||||
| adapterLocale={`${language}-hk`} | 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> | </LocalizationProvider> | ||||
| ); | ); | ||||
| }} | }} | ||||
| @@ -442,6 +523,13 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| </Grid> */} | </Grid> */} | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| ); | |||||
| <CalculateExpiryDateModal | |||||
| open={openModal} | |||||
| onClose={handleOnModalClose} | |||||
| onSubmit={handleReturnExpiryDate} | |||||
| textfieldSx={textfieldSx} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | }; | ||||
| export default FgStockInForm; | export default FgStockInForm; | ||||
| @@ -1,5 +1,4 @@ | |||||
| { | { | ||||
| "dashboard": "資訊展示面板", | "dashboard": "資訊展示面板", | ||||
| "Edit": "編輯", | "Edit": "編輯", | ||||
| "Job Order Production Process": "工單生產流程", | "Job Order Production Process": "工單生產流程", | ||||
| @@ -7,12 +6,28 @@ | |||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "All": "全部", | "All": "全部", | ||||
| "No options": "沒有選項", | "No options": "沒有選項", | ||||
| "Finished QC Job Orders": "完成QC工單", | |||||
| "Reset": "重置", | "Reset": "重置", | ||||
| "Search": "搜尋", | "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": "編號", | "Code": "編號", | ||||
| "Staff No": "員工編號", | |||||
| "code": "編號", | "code": "編號", | ||||
| "Name": "名稱", | "Name": "名稱", | ||||
| "Assignment successful": "分配成功", | "Assignment successful": "分配成功", | ||||
| "Pass": "通過", | |||||
| "Unable to get user ID": "無法獲取用戶ID", | "Unable to get user ID": "無法獲取用戶ID", | ||||
| "Unknown error: ": "未知錯誤: ", | "Unknown error: ": "未知錯誤: ", | ||||
| "Please try again later.": "請稍後重試。", | "Please try again later.": "請稍後重試。", | ||||
| @@ -25,7 +40,6 @@ | |||||
| "R&D": "研發", | "R&D": "研發", | ||||
| "STF": "樣品", | "STF": "樣品", | ||||
| "Other": "其他", | "Other": "其他", | ||||
| "Add some entries!": "添加條目", | "Add some entries!": "添加條目", | ||||
| "Add Record": "新增", | "Add Record": "新增", | ||||
| "Clean Record": "重置", | "Clean Record": "重置", | ||||
| @@ -49,19 +63,42 @@ | |||||
| "Changeover Time": "生產後轉換時間", | "Changeover Time": "生產後轉換時間", | ||||
| "Warehouse": "倉庫", | "Warehouse": "倉庫", | ||||
| "Supplier": "供應商", | "Supplier": "供應商", | ||||
| "Purchase Order":"採購單", | |||||
| "Demand Forecast":"需求預測", | |||||
| "Purchase Order": "採購單", | |||||
| "Demand Forecast": "需求預測", | |||||
| "Pick Order": "提料單", | "Pick Order": "提料單", | ||||
| "Deliver Order":"送貨訂單", | |||||
| "Project":"專案", | |||||
| "Product":"產品", | |||||
| "Material":"材料", | |||||
| "mat":"原料", | |||||
| "Deliver Order": "送貨訂單", | |||||
| "Project": "專案", | |||||
| "Product": "產品", | |||||
| "Material": "材料", | |||||
| "mat": "原料", | |||||
| "consumables": "消耗品", | "consumables": "消耗品", | ||||
| "non-consumables": "非消耗品", | "non-consumables": "非消耗品", | ||||
| "fg": "成品", | "fg": "成品", | ||||
| "sfg": "半成品", | "sfg": "半成品", | ||||
| "item": "貨品", | "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":"成品", | "FG":"成品", | ||||
| "Qty":"數量", | "Qty":"數量", | ||||
| "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | ||||
| @@ -87,7 +124,7 @@ | |||||
| "Qc Item": "QC 項目", | "Qc Item": "QC 項目", | ||||
| "FG Production Schedule": "FG 生產排程", | "FG Production Schedule": "FG 生產排程", | ||||
| "Inventory": "庫存", | "Inventory": "庫存", | ||||
| "scheduling":"排程", | |||||
| "scheduling": "排程", | |||||
| "settings": "設定", | "settings": "設定", | ||||
| "items": "物料", | "items": "物料", | ||||
| "edit":"編輯", | "edit":"編輯", | ||||
| @@ -95,6 +132,11 @@ | |||||
| "Edit Equipment":"設備詳情", | "Edit Equipment":"設備詳情", | ||||
| "equipmentType":"設備種類", | "equipmentType":"設備種類", | ||||
| "Description":"描述", | "Description":"描述", | ||||
| "edit": "編輯", | |||||
| "Edit Equipment Type": "設備類型詳情", | |||||
| "Edit Equipment": "設備詳情", | |||||
| "equipmentType": "設備類型", | |||||
| "Description": "描述", | |||||
| "Details": "詳情", | "Details": "詳情", | ||||
| "Equipment Type Details":"設備類型詳情", | "Equipment Type Details":"設備類型詳情", | ||||
| "Equipment Type":"設備類型", | "Equipment Type":"設備類型", | ||||
| @@ -103,6 +145,12 @@ | |||||
| "Equipment Details":"設備詳情", | "Equipment Details":"設備詳情", | ||||
| "Exclude Date":"排除日期", | "Exclude Date":"排除日期", | ||||
| "Finished Goods Name":"成品名稱", | "Finished Goods Name":"成品名稱", | ||||
| "Equipment Type Details": "設備類型詳情", | |||||
| "Save": "儲存", | |||||
| "Cancel": "取消", | |||||
| "Equipment Details": "設備詳情", | |||||
| "Exclude Date": "排除日期", | |||||
| "Finished Goods Name": "成品名稱", | |||||
| "create": "新增", | "create": "新增", | ||||
| "hr": "小時", | "hr": "小時", | ||||
| "hrs": "小時", | "hrs": "小時", | ||||
| @@ -125,7 +173,6 @@ | |||||
| "Stop Scan": "停止掃碼", | "Stop Scan": "停止掃碼", | ||||
| "Scan Result": "掃碼結果", | "Scan Result": "掃碼結果", | ||||
| "Expiry Date": "有效期", | "Expiry Date": "有效期", | ||||
| "Pick Order Code": "提料單編號", | "Pick Order Code": "提料單編號", | ||||
| "Target Date": "需求日期", | "Target Date": "需求日期", | ||||
| "Lot Required Pick Qty": "批號需求數量", | "Lot Required Pick Qty": "批號需求數量", | ||||
| @@ -135,8 +182,6 @@ | |||||
| "No data available": "沒有資料", | "No data available": "沒有資料", | ||||
| "jodetail": "工單細節", | "jodetail": "工單細節", | ||||
| "Sign out": "登出", | "Sign out": "登出", | ||||
| "By-product": "副產品", | "By-product": "副產品", | ||||
| "Complete Step": "完成步驟", | "Complete Step": "完成步驟", | ||||
| "Defect": "不良品", | "Defect": "不良品", | ||||
| @@ -163,7 +208,6 @@ | |||||
| "Output Qty": "輸出數量", | "Output Qty": "輸出數量", | ||||
| "Pending": "待處理", | "Pending": "待處理", | ||||
| "pending": "待處理", | "pending": "待處理", | ||||
| "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | ||||
| "Please scan operator code": "請掃描操作員編號", | "Please scan operator code": "請掃描操作員編號", | ||||
| "Please scan operator code first": "請先掃描操作員編號", | "Please scan operator code first": "請先掃描操作員編號", | ||||
| @@ -171,15 +215,13 @@ | |||||
| "Production Process Information": "生產流程信息", | "Production Process Information": "生產流程信息", | ||||
| "Production Process Steps": "生產流程步驟", | "Production Process Steps": "生產流程步驟", | ||||
| "Scan Operator & Equipment": "掃描操作員和設備", | "Scan Operator & Equipment": "掃描操作員和設備", | ||||
| "Seq": "序號", | |||||
| "Setup Time (mins)": "生產前預備時間(分鐘)", | "Setup Time (mins)": "生產前預備時間(分鐘)", | ||||
| "Start": "開始", | "Start": "開始", | ||||
| "Start QR Scan": "開始掃碼", | "Start QR Scan": "開始掃碼", | ||||
| "Status": "狀態", | |||||
| "Status": "狀態", | |||||
| "in_progress": "進行中", | "in_progress": "進行中", | ||||
| "In_Progress": "進行中", | "In_Progress": "進行中", | ||||
| "inProgress": "進行中", | "inProgress": "進行中", | ||||
| "Step Name": "名稱", | "Step Name": "名稱", | ||||
| "Stop QR Scan": "停止掃碼", | "Stop QR Scan": "停止掃碼", | ||||
| "Submit & Start": "提交並開始", | "Submit & Start": "提交並開始", | ||||
| @@ -188,10 +230,13 @@ | |||||
| "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | ||||
| "View": "查看", | "View": "查看", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "BoM Material": "物料清單", | |||||
| "BoM Material": "成品/半成品清單", | |||||
| "N/A": "不適用", | "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": "物料名稱", | "Item Name": "物料名稱", | ||||
| "Job Order Info": "工單信息", | "Job Order Info": "工單信息", | ||||
| "Matching Stock": "工單對料", | "Matching Stock": "工單對料", | ||||
| @@ -223,6 +268,7 @@ | |||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "view stockin": "品檢", | "view stockin": "品檢", | ||||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | "No completed Job Order pick orders with matching found": "沒有相關記錄", | ||||
| "Handler": "提料員", | |||||
| "Completed Step": "完成步驟", | "Completed Step": "完成步驟", | ||||
| "Continue": "繼續", | "Continue": "繼續", | ||||
| "Executing": "執行中", | "Executing": "執行中", | ||||
| @@ -235,4 +281,4 @@ | |||||
| "Lines with sufficient stock: ": "可提料項目數量: ", | "Lines with sufficient stock: ": "可提料項目數量: ", | ||||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | "Lines with insufficient stock: ": "未能提料項目數量: ", | ||||
| "Total lines: ": "總數量:" | "Total lines: ": "總數量:" | ||||
| } | |||||
| } | |||||
| @@ -8,11 +8,19 @@ | |||||
| "Code": "工單編號", | "Code": "工單編號", | ||||
| "Name": "成品/半成品名稱", | "Name": "成品/半成品名稱", | ||||
| "Picked Qty": "已提料數量", | "Picked Qty": "已提料數量", | ||||
| "Req. Qty": "需求數量", | |||||
| "Confirm All": "確認所有提料", | |||||
| "UoM": "銷售單位", | "UoM": "銷售單位", | ||||
| "No": "沒有", | "No": "沒有", | ||||
| "User not found with staffNo:": "用戶不存在", | |||||
| "Time Remaining": "剩餘時間", | |||||
| "Over Time": "超時", | |||||
| "Staff No:": "員工編號:", | |||||
| "Timer Paused": "計時器已暫停", | |||||
| "Staff No Required": "員工編號必填", | |||||
| "Staff No": "員工編號", | |||||
| "Status": "工單狀態", | "Status": "工單狀態", | ||||
| "Lot No.": "批號", | "Lot No.": "批號", | ||||
| "Pass": "通過", | |||||
| "Delete Job Order": "刪除工單", | "Delete Job Order": "刪除工單", | ||||
| "Bom": "半成品/成品編號", | "Bom": "半成品/成品編號", | ||||
| "Release": "放單", | "Release": "放單", | ||||
| @@ -86,6 +94,8 @@ | |||||
| "Job Order Item Name": "工單物料名稱", | "Job Order Item Name": "工單物料名稱", | ||||
| "Job Order Code": "工單編號", | "Job Order Code": "工單編號", | ||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "Skip": "跳過", | |||||
| "Handler": "提料員", | |||||
| "Required Qty": "需求數量", | "Required Qty": "需求數量", | ||||
| "completed Job Order pick orders with Matching": "工單已完成提料和對料", | "completed Job Order pick orders with Matching": "工單已完成提料和對料", | ||||
| "No completed Job Order pick orders with matching found": "沒有相關記錄", | "No completed Job Order pick orders with matching found": "沒有相關記錄", | ||||
| @@ -134,7 +144,7 @@ | |||||
| "Confirm Lot Substitution": "確認批號替換", | "Confirm Lot Substitution": "確認批號替換", | ||||
| "Processing...": "處理中", | "Processing...": "處理中", | ||||
| "Complete Job Order Record": "已完成工單記錄", | "Complete Job Order Record": "已完成工單記錄", | ||||
| "Back": "返回", | |||||
| "Lot Details": "批號細節", | "Lot Details": "批號細節", | ||||
| "No lot details available": "沒有批號細節", | "No lot details available": "沒有批號細節", | ||||
| "Second Scan Completed": "對料已完成", | "Second Scan Completed": "對料已完成", | ||||
| @@ -146,7 +156,9 @@ | |||||
| "Reject": "拒絕", | "Reject": "拒絕", | ||||
| "Stock Unit": "庫存單位", | "Stock Unit": "庫存單位", | ||||
| "Group": "組", | "Group": "組", | ||||
| "Item": "物料", | |||||
| "Input Equipment is not match with process": "輸入的設備與流程不匹配", | |||||
| "Item": "成品/半成品", | |||||
| "Select Date": "選擇日期", | |||||
| "No Group": "沒有組", | "No Group": "沒有組", | ||||
| "No created items": "沒有創建物料", | "No created items": "沒有創建物料", | ||||
| "Order Quantity": "需求數量", | "Order Quantity": "需求數量", | ||||
| @@ -274,7 +286,6 @@ | |||||
| "acceptQty must not greater than": "接受數量不能大於", | "acceptQty must not greater than": "接受數量不能大於", | ||||
| "escalation": "升級", | "escalation": "升級", | ||||
| "failedQty": "失敗數量", | "failedQty": "失敗數量", | ||||
| "qcItem": "QC物料", | |||||
| "qcResult": "QC結果", | "qcResult": "QC結果", | ||||
| "remarks": "備註", | "remarks": "備註", | ||||
| "supervisor": "主管", | "supervisor": "主管", | ||||
| @@ -324,13 +335,15 @@ | |||||
| "pending": "待處理", | "pending": "待處理", | ||||
| "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | ||||
| "Please scan equipment code": "請掃描設備編號", | |||||
| "Equipment Code": "設備編號", | |||||
| "Please scan operator code": "請掃描操作員編號", | "Please scan operator code": "請掃描操作員編號", | ||||
| "Please scan operator code first": "請先掃描操作員編號", | "Please scan operator code first": "請先掃描操作員編號", | ||||
| "Processing Time (mins)": "步驟時間(分鐘)", | "Processing Time (mins)": "步驟時間(分鐘)", | ||||
| "Production Process Information": "生產流程信息", | "Production Process Information": "生產流程信息", | ||||
| "Production Process Steps": "生產流程步驟", | "Production Process Steps": "生產流程步驟", | ||||
| "Scan Operator & Equipment": "掃描操作員和設備", | "Scan Operator & Equipment": "掃描操作員和設備", | ||||
| "Seq": "序號", | |||||
| "Seq:": "步驟", | |||||
| "Setup Time (mins)": "生產前預備時間(分鐘)", | "Setup Time (mins)": "生產前預備時間(分鐘)", | ||||
| "Start": "開始", | "Start": "開始", | ||||
| "Start QR Scan": "開始掃碼", | "Start QR Scan": "開始掃碼", | ||||
| @@ -356,18 +369,13 @@ | |||||
| "View": "查看", | "View": "查看", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "N/A": "不適用", | "N/A": "不適用", | ||||
| "BoM Material": "物料清單", | |||||
| "BoM Material": "成品/半成品清單", | |||||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | ||||
| "Item Code": "物料編號", | |||||
| "Item Name": "物料名稱", | |||||
| "Enter the number of cartons: ": "請輸入箱數:", | "Enter the number of cartons: ": "請輸入箱數:", | ||||
| "Number of cartons": "箱數", | "Number of cartons": "箱數", | ||||
| "You need to enter a number": "您需要輸入一個數字", | "You need to enter a number": "您需要輸入一個數字", | ||||
| "Number must be at least 1": "數字必須至少為1", | "Number must be at least 1": "數字必須至少為1", | ||||
| "Confirm": "確認", | |||||
| "Cancel": "取消", | |||||
| "Print Pick Record": "打印板頭紙", | |||||
| "Printed Successfully.": "成功列印", | |||||
| "Job Order Info": "工單信息", | "Job Order Info": "工單信息", | ||||
| "Matching Stock": "工單對料", | "Matching Stock": "工單對料", | ||||
| "No data found": "沒有找到資料", | "No data found": "沒有找到資料", | ||||