| @@ -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 = (() => { | ||||
| @@ -215,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, | ||||
| @@ -260,6 +261,7 @@ export interface ProductProcessWithLinesResponse { | |||||
| outputQtyUom: string; | outputQtyUom: string; | ||||
| productionPriority: number; | productionPriority: number; | ||||
| jobOrderLines: JobOrderLineInfo[]; | jobOrderLines: JobOrderLineInfo[]; | ||||
| productProcessLines: ProductProcessLineResponse[]; | productProcessLines: ProductProcessLineResponse[]; | ||||
| } | } | ||||
| @@ -321,6 +323,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||||
| bomId?: number; | bomId?: number; | ||||
| assignedTo: number; | assignedTo: number; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| pickOrderStatus: string; | |||||
| itemName: string; | itemName: string; | ||||
| requiredQty: number; | requiredQty: number; | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| @@ -347,6 +350,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, | ||||
| @@ -556,7 +564,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`, | ||||
| @@ -658,6 +675,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`, | ||||
| @@ -879,7 +908,15 @@ export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const fetchJoForPrintQrCode = cache(async () => { | |||||
| return serverFetchJson<JobOrderListForPrintQrCodeResponse[]>( | |||||
| `${BASE_API_URL}/jo/joForPrintQrCode`, | |||||
| { | |||||
| 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[]>( | ||||
| @@ -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[]; | ||||
| @@ -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> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -575,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) { | ||||
| @@ -615,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) { | ||||
| @@ -635,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; | ||||
| @@ -1113,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 ? ( | ||||
| @@ -1167,7 +1192,7 @@ 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> | ||||
| @@ -1179,7 +1204,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| <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> | ||||
| @@ -1235,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' ? ( | ||||
| @@ -1269,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"> | ||||
| @@ -1280,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' | ||||
| @@ -1294,7 +1320,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||||
| minWidth: '70px' | minWidth: '70px' | ||||
| }} | }} | ||||
| > | > | ||||
| {t("Submit")} | |||||
| {t("Confirm")} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| @@ -101,6 +101,7 @@ interface LotDetail { | |||||
| itemName: string; | itemName: string; | ||||
| uomCode: string; | uomCode: string; | ||||
| uomDesc: string; | uomDesc: string; | ||||
| match_status: string; | |||||
| } | } | ||||
| const CompleteJobOrderRecord: React.FC<Props> = ({ | const CompleteJobOrderRecord: React.FC<Props> = ({ | ||||
| @@ -538,12 +539,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| 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', | ||||
| }, | }, | ||||
| @@ -65,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) | ||||
| @@ -322,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 }; | ||||
| @@ -1380,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 { | ||||
| @@ -1395,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(() => { | ||||
| @@ -0,0 +1,204 @@ | |||||
| "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, | |||||
| } 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 fetchJobOrders = useCallback(async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| const data = await fetchJoForPrintQrCode(); | |||||
| setJobOrders(data || []); | |||||
| setPage(0); | |||||
| } catch (e) { | |||||
| console.error(e); | |||||
| setJobOrders([]); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, []); | |||||
| 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> | |||||
| {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"; | ||||
| @@ -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,51 @@ 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; | |||||
| if (!effectiveEquipmentCode) { | |||||
| console.error("No equipment code available"); | |||||
| alert(t("Please scan equipment code or equipment detail ID")); | |||||
| setIsAutoSubmitting(false); | |||||
| if (autoSubmitTimerRef.current) { | |||||
| clearTimeout(autoSubmitTimerRef.current); | |||||
| autoSubmitTimerRef.current = null; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| 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 +369,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); | ||||
| @@ -451,15 +466,16 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| 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 +500,7 @@ 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) { | ||||
| @@ -764,9 +780,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 or equipment detail id") | |||||
| } | } | ||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| @@ -792,7 +809,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 || (!scannedEquipmentCode)} | |||||
| > | > | ||||
| {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; | ||||
| @@ -156,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); | ||||
| @@ -269,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,124 @@ 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; | |||||
| } | |||||
| if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { | if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { | ||||
| console.log("Line duration or start time is not valid"); | |||||
| setRemainingTime(null); | |||||
| setIsOverTime(false); | |||||
| return; | |||||
| } | |||||
| // 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"); | |||||
| // 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"); | |||||
| } | |||||
| // Check if date is valid | |||||
| if (isNaN(start.getTime())) { | |||||
| console.error("Invalid startTime:", lineDetail.startTime); | |||||
| 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); | |||||
| const durationMs = lineDetail.durationInMinutes * 60_000; | |||||
| // Check if line is paused | |||||
| const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused"; | |||||
| 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 current time | |||||
| if (!frozenRemainingTime) { | |||||
| const now = new Date(); | |||||
| const elapsed = now.getTime() - start.getTime(); | |||||
| 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); | |||||
| } 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); | |||||
| } | |||||
| } else { | |||||
| // Keep using frozen value while paused | |||||
| setRemainingTime(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) { | |||||
| setFrozenRemainingTime(null); | |||||
| setLastPauseTime(null); | |||||
| } | |||||
| const now = new Date(); | |||||
| const elapsed = now.getTime() - start.getTime(); | |||||
| const remaining = durationMs - elapsed; | |||||
| 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]); | |||||
| // 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 +274,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 +366,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,7 +404,7 @@ 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 }}> | ||||
| @@ -426,7 +574,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 +619,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 +628,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 +688,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> | ||||
| @@ -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 === "半成品"; | |||||
| 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: stockInLineInfo?.acceptedQty, // 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 { | ||||
| @@ -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,35 +63,35 @@ | |||||
| "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":"物料", | |||||
| "Production Date":"生產日期", | |||||
| "QC Check Item":"QC品檢項目", | |||||
| "QC Category":"QC品檢模板", | |||||
| "qcCategory":"品檢模板", | |||||
| "QC Check Template":"QC檢查模板", | |||||
| "Mail":"郵件", | |||||
| "Import Testing":"匯入測試", | |||||
| "FG": "成品", | |||||
| "Qty": "數量", | |||||
| "FG & Material Demand Forecast Detail": "成品及材料需求預測詳情", | |||||
| "View item In-out And inventory Ledger": "查看物料出入庫及庫存日誌", | |||||
| "Delivery Order": "送貨訂單", | |||||
| "Detail Scheduling": "詳細排程", | |||||
| "Customer": "客戶", | |||||
| "qcItem": "品檢項目", | |||||
| "Item": "物料", | |||||
| "Production Date": "生產日期", | |||||
| "QC Check Item": "QC品檢項目", | |||||
| "QC Category": "QC品檢模板", | |||||
| "qcCategory": "品檢模板", | |||||
| "QC Check Template": "QC檢查模板", | |||||
| "Mail": "郵件", | |||||
| "Import Testing": "匯入測試", | |||||
| "Overview": "總覽", | "Overview": "總覽", | ||||
| "Projects": "專案", | "Projects": "專案", | ||||
| "Create Project": "新增專案", | "Create Project": "新增專案", | ||||
| @@ -86,21 +100,21 @@ | |||||
| "Qc Item": "QC 項目", | "Qc Item": "QC 項目", | ||||
| "FG Production Schedule": "FG 生產排程", | "FG Production Schedule": "FG 生產排程", | ||||
| "Inventory": "庫存", | "Inventory": "庫存", | ||||
| "scheduling":"排程", | |||||
| "scheduling": "排程", | |||||
| "settings": "設定", | "settings": "設定", | ||||
| "items": "物料", | "items": "物料", | ||||
| "edit":"編輯", | |||||
| "Edit Equipment Type":"設備類型詳情", | |||||
| "Edit Equipment":"設備詳情", | |||||
| "equipmentType":"設備類型", | |||||
| "Description":"描述", | |||||
| "edit": "編輯", | |||||
| "Edit Equipment Type": "設備類型詳情", | |||||
| "Edit Equipment": "設備詳情", | |||||
| "equipmentType": "設備類型", | |||||
| "Description": "描述", | |||||
| "Details": "詳情", | "Details": "詳情", | ||||
| "Equipment Type Details":"設備類型詳情", | |||||
| "Save":"儲存", | |||||
| "Cancel":"取消", | |||||
| "Equipment Details":"設備詳情", | |||||
| "Exclude Date":"排除日期", | |||||
| "Finished Goods Name":"成品名稱", | |||||
| "Equipment Type Details": "設備類型詳情", | |||||
| "Save": "儲存", | |||||
| "Cancel": "取消", | |||||
| "Equipment Details": "設備詳情", | |||||
| "Exclude Date": "排除日期", | |||||
| "Finished Goods Name": "成品名稱", | |||||
| "create": "新增", | "create": "新增", | ||||
| "hr": "小時", | "hr": "小時", | ||||
| "hrs": "小時", | "hrs": "小時", | ||||
| @@ -123,7 +137,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": "批號需求數量", | ||||
| @@ -133,8 +146,6 @@ | |||||
| "No data available": "沒有資料", | "No data available": "沒有資料", | ||||
| "jodetail": "工單細節", | "jodetail": "工單細節", | ||||
| "Sign out": "登出", | "Sign out": "登出", | ||||
| "By-product": "副產品", | "By-product": "副產品", | ||||
| "Complete Step": "完成步驟", | "Complete Step": "完成步驟", | ||||
| "Defect": "不良品", | "Defect": "不良品", | ||||
| @@ -161,7 +172,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": "請先掃描操作員編號", | ||||
| @@ -173,11 +183,10 @@ | |||||
| "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,7 +197,7 @@ | |||||
| "Back": "返回", | "Back": "返回", | ||||
| "BoM Material": "物料清單", | "BoM Material": "物料清單", | ||||
| "N/A": "不適用", | "N/A": "不適用", | ||||
| "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 Code": "物料編號", | ||||
| "Item Name": "物料名稱", | "Item Name": "物料名稱", | ||||
| "Job Order Info": "工單信息", | "Job Order Info": "工單信息", | ||||
| @@ -234,4 +243,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": "放單", | ||||
| @@ -136,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": "對料已完成", | ||||
| @@ -366,7 +374,7 @@ | |||||
| "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": "取消", | "Cancel": "取消", | ||||
| "Print Pick Record": "打印板頭紙", | "Print Pick Record": "打印板頭紙", | ||||
| "Printed Successfully.": "成功列印", | "Printed Successfully.": "成功列印", | ||||