| @@ -25,10 +25,13 @@ const JoEdit: React.FC<Props> = async ({ searchParams }) => { | |||
| try { | |||
| await fetchJoDetail(parseInt(id)) | |||
| } catch (e) { | |||
| if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { | |||
| console.log(e) | |||
| notFound(); | |||
| console.log("Job Order not found:", e); | |||
| } else { | |||
| console.error("Error fetching Job Order detail:", e); | |||
| } | |||
| notFound(); | |||
| } | |||
| @@ -8,6 +8,7 @@ export interface BomCombo { | |||
| label: string; | |||
| outputQty: number; | |||
| outputQtyUom: string; | |||
| description: string; | |||
| } | |||
| export const preloadBomCombo = (() => { | |||
| @@ -215,6 +215,7 @@ export interface ProductProcessLineResponse { | |||
| seqNo: number, | |||
| name: string, | |||
| description: string, | |||
| equipmentDetailId: number, | |||
| equipment_name: string, | |||
| equipmentDetailCode: string, | |||
| status: string, | |||
| @@ -260,6 +261,7 @@ export interface ProductProcessWithLinesResponse { | |||
| outputQtyUom: string; | |||
| productionPriority: number; | |||
| jobOrderLines: JobOrderLineInfo[]; | |||
| productProcessLines: ProductProcessLineResponse[]; | |||
| } | |||
| @@ -321,6 +323,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||
| bomId?: number; | |||
| assignedTo: number; | |||
| pickOrderId: number; | |||
| pickOrderStatus: string; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| jobOrderId: number; | |||
| @@ -347,6 +350,11 @@ export interface ProductProcessLineQrscanUpadteRequest { | |||
| equipmentTypeSubTypeEquipmentNo?: string; | |||
| staffNo?: string; | |||
| } | |||
| export interface NewProductProcessLineQrscanUpadteRequest{ | |||
| productProcessLineId: number; | |||
| equipmentCode?: string; | |||
| staffNo?: string; | |||
| } | |||
| export interface ProductProcessLineDetailResponse { | |||
| id: number, | |||
| @@ -556,7 +564,16 @@ export interface LotDetailResponse { | |||
| matchQty?: number | null; | |||
| } | |||
| export interface JobOrderListForPrintQrCodeResponse { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| reqQty: number; | |||
| stockOutLineId: number; | |||
| stockOutLineQty: number; | |||
| stockOutLineStatus: string; | |||
| finihedTime: string; | |||
| } | |||
| export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, | |||
| @@ -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 () => { | |||
| return serverFetchJson<AllJoborderProductProcessInfoResponse[]>( | |||
| `${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 | |||
| export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { | |||
| return serverFetchJson<any[]>( | |||
| @@ -128,6 +128,7 @@ export interface StockInLine { | |||
| dnNo?: string; | |||
| dnDate?: number[]; | |||
| stockQty?: number; | |||
| bomDescription?: string; | |||
| handlerId?: number; | |||
| putAwayLines?: PutAwayLine[]; | |||
| qcResult?: QcResult[]; | |||
| @@ -8,14 +8,16 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| import { isFinite } from "lodash"; | |||
| import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react"; | |||
| import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react"; | |||
| import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { msg } from "../Swal/CustomAlerts"; | |||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||
| interface Props { | |||
| open: boolean; | |||
| bomCombo: BomCombo[]; | |||
| jobTypes: JobTypeResponse[]; | |||
| onClose: () => void; | |||
| onSearch: () => void; | |||
| } | |||
| @@ -23,6 +25,7 @@ interface Props { | |||
| const JoCreateFormModal: React.FC<Props> = ({ | |||
| open, | |||
| bomCombo, | |||
| jobTypes, | |||
| onClose, | |||
| onSearch, | |||
| }) => { | |||
| @@ -30,19 +33,130 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| const formProps = useForm<SaveJo>({ | |||
| mode: "onChange", | |||
| }); | |||
| const { reset, trigger, watch, control, register, formState: { errors } } = formProps | |||
| const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps | |||
| // 监听 bomId 变化 | |||
| const selectedBomId = watch("bomId"); | |||
| const onModalClose = useCallback(() => { | |||
| reset() | |||
| onClose() | |||
| }, []) | |||
| }, [reset, onClose]) | |||
| const handleAutoCompleteChange = useCallback( | |||
| (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | |||
| console.log("BOM changed to:", value); | |||
| onChange(value.id); | |||
| // 1) 根据 BOM 设置数量 | |||
| if (value.outputQty != null) { | |||
| formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }); | |||
| } | |||
| const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | |||
| onChange(value.id) | |||
| if (value.outputQty != null) { | |||
| formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }) | |||
| // 2) 选 BOM 时,把日期默认设为“今天” | |||
| const today = dayjs(); | |||
| const todayStr = dayjsToDateString(today, "input"); // 你已经有的工具函数 | |||
| formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true }); | |||
| }, | |||
| [formProps] | |||
| ); | |||
| // 使用 useMemo 来计算过滤后的 jobTypes,响应 selectedBomId 变化 | |||
| /* | |||
| const filteredJobTypes = useMemo(() => { | |||
| console.log("getFilteredJobTypes called, selectedBomId:", selectedBomId); | |||
| if (!selectedBomId) { | |||
| console.log("No BOM selected, returning all jobTypes:", jobTypes); | |||
| return jobTypes; | |||
| } | |||
| }, []) | |||
| const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); | |||
| console.log("Selected BOM:", selectedBom); | |||
| console.log("Selected BOM full object:", JSON.stringify(selectedBom, null, 2)); | |||
| if (!selectedBom) { | |||
| console.log("BOM not found, returning all jobTypes"); | |||
| return jobTypes; | |||
| } | |||
| // 检查 description 是否存在 | |||
| const description = selectedBom.description; | |||
| console.log("BOM description (raw):", description); | |||
| console.log("BOM description type:", typeof description); | |||
| console.log("BOM description is undefined?", description === undefined); | |||
| console.log("BOM description is null?", description === null); | |||
| if (!description) { | |||
| console.log("BOM description is missing or empty, returning all jobTypes"); | |||
| return jobTypes; | |||
| } | |||
| const descriptionUpper = description.toUpperCase(); | |||
| console.log("BOM description (uppercase):", descriptionUpper); | |||
| console.log("All jobTypes:", jobTypes); | |||
| let filtered: JobTypeResponse[] = []; | |||
| if (descriptionUpper === "WIP") { | |||
| filtered = jobTypes.filter(jt => { | |||
| const jobTypeName = jt.name.toUpperCase(); | |||
| const shouldInclude = jobTypeName !== "FG"; | |||
| console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); | |||
| return shouldInclude; | |||
| }); | |||
| } else if (descriptionUpper === "FG") { | |||
| filtered = jobTypes.filter(jt => { | |||
| const jobTypeName = jt.name.toUpperCase(); | |||
| const shouldInclude = jobTypeName !== "WIP"; | |||
| console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); | |||
| return shouldInclude; | |||
| }); | |||
| } else { | |||
| filtered = jobTypes; | |||
| } | |||
| console.log("Filtered jobTypes:", filtered); | |||
| return filtered; | |||
| }, [bomCombo, jobTypes, selectedBomId]); | |||
| */ | |||
| // 当 BOM 改变时,自动选择匹配的 Job Type | |||
| useEffect(() => { | |||
| if (!selectedBomId) { | |||
| return; | |||
| } | |||
| const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); | |||
| if (!selectedBom) { | |||
| return; | |||
| } | |||
| const description = selectedBom.description; | |||
| console.log("Auto-select effect - BOM description:", description); | |||
| if (!description) { | |||
| console.log("Auto-select effect - No description found, skipping auto-select"); | |||
| return; | |||
| } | |||
| const descriptionUpper = description.toUpperCase(); | |||
| console.log("Auto-selecting Job Type for BOM description:", descriptionUpper); | |||
| // 查找匹配的 Job Type | |||
| const matchingJobType = jobTypes.find(jt => { | |||
| const jobTypeName = jt.name.toUpperCase(); | |||
| const matches = jobTypeName === descriptionUpper; | |||
| console.log(`Checking JobType ${jt.name} (${jobTypeName}) against ${descriptionUpper}: ${matches}`); | |||
| return matches; | |||
| }); | |||
| if (matchingJobType) { | |||
| console.log("Found matching Job Type, setting jobTypeId to:", matchingJobType.id); | |||
| setValue("jobTypeId", matchingJobType.id, { shouldValidate: true, shouldDirty: true }); | |||
| } else { | |||
| console.log("No matching Job Type found for description:", descriptionUpper); | |||
| } | |||
| }, [selectedBomId, bomCombo, jobTypes, setValue]); | |||
| const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { | |||
| if (value != null) { | |||
| @@ -98,7 +212,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| "flex-direction": "column", | |||
| flexDirection: "column", | |||
| padding: "20px", | |||
| height: "100%", //'30rem', | |||
| width: "100%", | |||
| @@ -199,36 +313,42 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={6}> | |||
| <Controller | |||
| control={control} | |||
| name="jobTypeId" | |||
| rules={{ required: t("Job Type required!") as string }} | |||
| render={({ field, fieldState: { error } }) => ( | |||
| <FormControl fullWidth error={Boolean(error)}> | |||
| <InputLabel>{t("Job Type")}</InputLabel> | |||
| <Select | |||
| <Controller | |||
| control={control} | |||
| name="jobTypeId" | |||
| rules={{ required: t("Job Type required!") as string }} | |||
| render={({ field, fieldState: { error } }) => { | |||
| //console.log("Job Type Select render - filteredJobTypes:", filteredJobTypes); | |||
| //console.log("Current field.value:", field.value); | |||
| return ( | |||
| <FormControl fullWidth error={Boolean(error)}> | |||
| <InputLabel>{t("Job Type")}</InputLabel> | |||
| <Select | |||
| {...field} | |||
| label={t("Job Type")} | |||
| value={field.value?.toString() ?? ""} | |||
| onChange={(event) => { | |||
| const value = event.target.value; | |||
| console.log("Job Type changed to:", value); | |||
| field.onChange(value === "" ? undefined : Number(value)); | |||
| }} | |||
| > | |||
| > | |||
| <MenuItem value=""> | |||
| <em>{t("Please select")}</em> | |||
| </MenuItem> | |||
| <MenuItem value="1">{t("FG")}</MenuItem> | |||
| <MenuItem value="2">{t("WIP")}</MenuItem> | |||
| <MenuItem value="3">{t("R&D")}</MenuItem> | |||
| <MenuItem value="4">{t("STF")}</MenuItem> | |||
| <MenuItem value="5">{t("Other")}</MenuItem> | |||
| </Select> | |||
| {/*{error && <FormHelperText>{error.message}</FormHelperText>}*/} | |||
| </FormControl> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| {/* {filteredJobTypes.map((jobType) => (*/} | |||
| {jobTypes.map((jobType) => ( | |||
| <MenuItem key={jobType.id} value={jobType.id.toString()}> | |||
| {t(jobType.name)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={6}> | |||
| <Controller | |||
| control={control} | |||
| @@ -398,6 +398,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| <JoCreateFormModal | |||
| open={isCreateJoModalOpen} | |||
| bomCombo={bomCombo} | |||
| jobTypes={jobTypes} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={() => { | |||
| setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 | |||
| @@ -47,7 +47,10 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| useEffect(() => { | |||
| fetchPickOrders(); | |||
| }, [fetchPickOrders]); | |||
| const handleBackToList = useCallback(() => { | |||
| setSelectedPickOrderId(undefined); | |||
| setSelectedJobOrderId(undefined); | |||
| }, []); | |||
| // If a pick order is selected, show JobPickExecution detail view | |||
| if (selectedPickOrderId !== undefined) { | |||
| return ( | |||
| @@ -64,7 +67,11 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} /> | |||
| <JobPickExecution | |||
| filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | |||
| //onSwitchToRecordTab={onSwitchToRecordTab} | |||
| onBackToList={handleBackToList} // 传递新的回调 | |||
| /> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -575,7 +575,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| const handleSubmitAllScanned = useCallback(async () => { | |||
| const scannedLots = combinedLotData.filter(lot => | |||
| lot.matchStatus === 'scanned' | |||
| lot.matchStatus === 'scanned'|| | |||
| lot.stockOutLineStatus === 'completed' | |||
| ); | |||
| if (scannedLots.length === 0) { | |||
| @@ -615,7 +616,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| if (successCount > 0) { | |||
| setQrScanSuccess(true); | |||
| setTimeout(() => setQrScanSuccess(false), 2000); | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| // 添加:提交成功后返回到列表 | |||
| if (onBack) { | |||
| onBack(); | |||
| } | |||
| }, 2000); | |||
| } | |||
| } catch (error: any) { | |||
| @@ -635,7 +642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| } finally { | |||
| setIsSubmittingAll(false); | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign]); | |||
| }, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign, onBack]); | |||
| const scannedItemsCount = useMemo(() => { | |||
| return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; | |||
| @@ -1113,7 +1120,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| )} | |||
| {/* Combined Lot Table */} | |||
| <Box> | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| onClick={handleSubmitAllScanned} | |||
| disabled={isSubmittingAll} | |||
| sx={{ minWidth: '160px' }} | |||
| > | |||
| {isSubmittingAll ? ( | |||
| <> | |||
| <CircularProgress size={16} sx={{ mr: 1 }} /> | |||
| {t("Submitting...")} | |||
| </> | |||
| ) : ( | |||
| t("Confirm All") | |||
| )} | |||
| </Button> | |||
| {/* | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}> | |||
| {!isManualScanning ? ( | |||
| @@ -1167,7 +1192,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| */} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| @@ -1179,7 +1204,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| <TableCell align="center">{t("Scan Result")}</TableCell> | |||
| {/* <TableCell align="center">{t("Scan Result")}</TableCell> */} | |||
| <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -1235,7 +1260,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; | |||
| })()} | |||
| </TableCell> | |||
| {/* | |||
| <TableCell align="center"> | |||
| {lot.matchStatus?.toLowerCase() === 'scanned' || | |||
| lot.matchStatus?.toLowerCase() === 'completed' ? ( | |||
| @@ -1269,7 +1294,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| </Typography> | |||
| )} | |||
| </TableCell> | |||
| */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ display: 'flex', justifyContent: 'center' }}> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| @@ -1280,9 +1305,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||
| handlePickQtyChange(lotKey, submitQty); | |||
| handleSubmitPickQtyWithQty(lot, submitQty); | |||
| updateSecondQrScanStatus(lot.pickOrderLineId, lot.lotId, currentUserId || 0, submitQty); | |||
| }} | |||
| disabled={ | |||
| lot.matchStatus !== 'scanned' || | |||
| //lot.matchStatus !== 'scanned' || | |||
| lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected' | |||
| @@ -1294,7 +1320,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => { | |||
| minWidth: '70px' | |||
| }} | |||
| > | |||
| {t("Submit")} | |||
| {t("Confirm")} | |||
| </Button> | |||
| <Button | |||
| @@ -101,6 +101,7 @@ interface LotDetail { | |||
| itemName: string; | |||
| uomCode: string; | |||
| uomDesc: string; | |||
| match_status: string; | |||
| } | |||
| const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| @@ -538,12 +539,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| height: '100%' | |||
| }}> | |||
| <Checkbox | |||
| checked={lot.secondQrScanStatus === 'completed'} | |||
| checked={lot.match_status === 'completed'} | |||
| disabled={true} | |||
| readOnly={true} | |||
| size="large" | |||
| sx={{ | |||
| color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400', | |||
| color: lot.match_status === 'completed' ? 'success.main' : 'grey.400', | |||
| '&.Mui-checked': { | |||
| color: 'success.main', | |||
| }, | |||
| @@ -65,7 +65,8 @@ import FGPickOrderCard from "./FGPickOrderCard"; | |||
| import LotConfirmationModal from "./LotConfirmationModal"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| onSwitchToRecordTab: () => void; | |||
| //onSwitchToRecordTab: () => void; | |||
| onBackToList?: () => void; | |||
| } | |||
| // QR Code Modal Component (from GoodPickExecution) | |||
| @@ -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 router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -1380,8 +1381,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| checkAndAutoAssignNext(); | |||
| if (onSwitchToRecordTab) { | |||
| onSwitchToRecordTab(); | |||
| if (onBackToList) { | |||
| onBackToList(); | |||
| } | |||
| }, 2000); | |||
| } else { | |||
| @@ -1395,7 +1396,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) | |||
| } finally { | |||
| setIsSubmittingAll(false); | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab]) | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList]) | |||
| // Calculate scanned items count | |||
| const scannedItemsCount = useMemo(() => { | |||
| @@ -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 { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { | |||
| fetchProductProcessById, | |||
| updateProductProcessLineQrscan, | |||
| // updateProductProcessLineQrscan, | |||
| newUpdateProductProcessLineQrscan, | |||
| fetchProductProcessLineDetail, | |||
| ProductProcessLineDetailResponse, | |||
| JobOrderProcessLineDetailResponse, | |||
| updateLineOutput, | |||
| ProductProcessLineInfoResponse, | |||
| ProductProcessResponse, | |||
| ProductProcessLineResponse, | |||
| completeProductProcessLine, | |||
| startProductProcessLine, | |||
| fetchProductProcessesByJobOrderId | |||
| } from "@/app/api/jo/actions"; | |||
| @@ -80,8 +75,10 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null); | |||
| const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null); | |||
| const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null); | |||
| // const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null); | |||
| const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null); | |||
| // const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState<number | null>(null); | |||
| const [scannedEquipmentCode, setScannedEquipmentCode] = useState<string | null>(null); | |||
| const [scanningLineId, setScanningLineId] = useState<number | null>(null); | |||
| const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const [showScanDialog, setShowScanDialog] = useState(false); | |||
| @@ -224,7 +221,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| const currentLine = lines.find(l => l.id === lineId); | |||
| if (currentLine && currentLine.equipment_name) { | |||
| const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`; | |||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); | |||
| setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); | |||
| console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`); | |||
| } else { | |||
| // 如果找不到 line,尝试从 API 获取 line detail | |||
| @@ -232,11 +229,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| fetchProductProcessLineDetail(lineId) | |||
| .then((lineDetail) => { | |||
| // 从 lineDetail 中获取 equipment_name | |||
| // 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整 | |||
| const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; | |||
| if (equipmentName) { | |||
| const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; | |||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo); | |||
| setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); | |||
| console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`); | |||
| } else { | |||
| console.warn(`Equipment name not found in line detail for lineId: ${lineId}`); | |||
| @@ -249,7 +245,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| return; | |||
| } | |||
| // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo | |||
| // 例如:{2fitestu123} = staffNo: "123" | |||
| // 例如:{2fitestustaff001} = staffNo: "staff001" | |||
| @@ -271,11 +266,11 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| return; | |||
| } | |||
| // 检查 equipmentTypeSubTypeEquipmentNo 格式 | |||
| const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo):\s*(.+)$/i); | |||
| // 检查 equipmentCode 格式 | |||
| const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i); | |||
| if (equipmentCodeMatch) { | |||
| const equipmentCode = equipmentCodeMatch[1].trim(); | |||
| setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode); | |||
| setScannedEquipmentCode(equipmentCode); | |||
| return; | |||
| } | |||
| @@ -286,11 +281,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| setScannedStaffNo(String(qrData.staffNo)); | |||
| } | |||
| if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) { | |||
| setScannedEquipmentTypeSubTypeEquipmentNo( | |||
| setScannedEquipmentCode( | |||
| String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode) | |||
| ); | |||
| } | |||
| // TODO: 处理 JSON 格式的 QR 码 | |||
| } catch { | |||
| // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode | |||
| if (trimmedValue.length > 0) { | |||
| @@ -299,7 +293,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| setScannedStaffNo(trimmedValue); | |||
| } else if (trimmedValue.includes("-")) { | |||
| // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") | |||
| setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue); | |||
| setScannedEquipmentCode(trimmedValue); | |||
| } | |||
| } | |||
| } | |||
| @@ -323,36 +317,51 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| console.log("submitScanAndStart called with:", { | |||
| lineId, | |||
| scannedStaffNo, | |||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||
| // scannedEquipmentTypeSubTypeEquipmentNo, | |||
| scannedEquipmentCode, | |||
| }); | |||
| if (!scannedStaffNo) { | |||
| console.log("No staffNo, cannot submit"); | |||
| setIsAutoSubmitting(false); | |||
| return false; // 没有 staffNo,不能提交 | |||
| return false; | |||
| } | |||
| try { | |||
| // 获取 line detail 以检查 bomProcessEquipmentId | |||
| const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); | |||
| // 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo | |||
| console.log("Submitting scan data:", { | |||
| // ✅ 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo) | |||
| const effectiveEquipmentCode = | |||
| scannedEquipmentCode ?? null; | |||
| 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, | |||
| staffNo: scannedStaffNo, | |||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo, | |||
| equipmentCode: effectiveEquipmentCode, | |||
| }); | |||
| const response = await updateProductProcessLineQrscan({ | |||
| const response = await newUpdateProductProcessLineQrscan({ | |||
| productProcessLineId: lineId, | |||
| equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined, | |||
| staffNo: scannedStaffNo || undefined, | |||
| equipmentCode: effectiveEquipmentCode, | |||
| staffNo: scannedStaffNo, | |||
| }); | |||
| console.log("Scan submit response:", response); | |||
| // 检查响应中的 message 字段来判断是否成功 | |||
| if (response && response.message) { | |||
| if (response && response.type === "error") { | |||
| console.error("Scan validation failed:", response.message); | |||
| alert(t(response.message) || t("Validation failed. Please check your input.")); | |||
| setIsAutoSubmitting(false); | |||
| if (autoSubmitTimerRef.current) { | |||
| clearTimeout(autoSubmitTimerRef.current); | |||
| @@ -360,25 +369,31 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| } | |||
| return false; | |||
| } | |||
| // 验证通过,继续执行后续步骤 | |||
| console.log("Validation passed, starting line..."); | |||
| handleStopScan(); | |||
| setShowScanDialog(false); | |||
| setIsAutoSubmitting(false); | |||
| await handleStartLine(lineId); | |||
| setSelectedLineId(lineId); | |||
| setIsExecutingLine(true); | |||
| await fetchProcessDetail(); | |||
| return true; | |||
| } catch (error) { | |||
| console.error("Error submitting scan:", error); | |||
| alert("Failed to submit scan data. Please try again."); | |||
| setIsAutoSubmitting(false); | |||
| return false; | |||
| } | |||
| }, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]); | |||
| }, [ | |||
| scannedStaffNo, | |||
| scannedEquipmentCode, | |||
| lineDetailForScan, | |||
| t, | |||
| fetchProcessDetail, | |||
| ]); | |||
| const handleSubmitScanAndStart = useCallback(async (lineId: number) => { | |||
| console.log("handleSubmitScanAndStart called with lineId:", lineId); | |||
| @@ -451,15 +466,16 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| console.log("Auto-submit check:", { | |||
| scanningLineId, | |||
| scannedStaffNo, | |||
| scannedEquipmentTypeSubTypeEquipmentNo, | |||
| scannedEquipmentCode, | |||
| isAutoSubmitting, | |||
| isManualScanning, | |||
| }); | |||
| // ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId | |||
| if ( | |||
| scanningLineId && | |||
| scannedStaffNo !== null && | |||
| scannedEquipmentTypeSubTypeEquipmentNo !== null && | |||
| (scannedEquipmentCode !== null) && | |||
| !isAutoSubmitting && | |||
| isManualScanning | |||
| ) { | |||
| @@ -484,7 +500,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| // 注意:这里不立即清除定时器,因为我们需要它执行 | |||
| // 只在组件卸载时清除 | |||
| }; | |||
| }, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]); | |||
| }, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]); | |||
| useEffect(() => { | |||
| return () => { | |||
| if (autoSubmitTimerRef.current) { | |||
| @@ -764,9 +780,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {scannedEquipmentTypeSubTypeEquipmentNo | |||
| ? `${t("Equipment Type/Code")}: ${scannedEquipmentTypeSubTypeEquipmentNo}` | |||
| : t("Please scan equipment code (optional if not required)") | |||
| {/* ✅ Show both options */} | |||
| {scannedEquipmentCode | |||
| ? `${t("Equipment Code")}: ${scannedEquipmentCode}` | |||
| : t("Please scan equipment code or equipment detail id") | |||
| } | |||
| </Typography> | |||
| </Box> | |||
| @@ -792,7 +809,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)} | |||
| disabled={!scannedStaffNo} | |||
| disabled={!scannedStaffNo || (!scannedEquipmentCode)} | |||
| > | |||
| {t("Submit & Start")} | |||
| </Button> | |||
| @@ -115,7 +115,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| }, [fetchData]); | |||
| // PickTable 组件内容 | |||
| const getStockAvailable = (line: JobOrderLine) => { | |||
| if (line.type?.toLowerCase() === "consumables") { | |||
| if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { | |||
| return null; | |||
| } | |||
| const inventory = inventoryData.find(inv => | |||
| @@ -158,7 +158,7 @@ const isStockSufficient = (line: JobOrderLine) => { | |||
| const stockCounts = useMemo(() => { | |||
| // 过滤掉 consumables 类型的 lines | |||
| const nonConsumablesLines = jobOrderLines.filter( | |||
| line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" | |||
| line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm" | |||
| ); | |||
| const total = nonConsumablesLines.length; | |||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | |||
| @@ -156,9 +156,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| const closeNewModal = useCallback(() => { | |||
| // const response = updateJo({ id: 1, status: "storing" }); | |||
| setOpenModal(false); // Close the modal first | |||
| fetchProcesses(); | |||
| // setTimeout(() => { | |||
| // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | |||
| }, []); | |||
| }, [fetchProcesses]); | |||
| const startIdx = page * PER_PAGE; | |||
| const paged = processes.slice(startIdx, startIdx + PER_PAGE); | |||
| @@ -269,7 +270,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| disabled={process.assignedTo != null || process.matchStatus == "completed"} | |||
| disabled={process.assignedTo != null || process.matchStatus == "completed"|| process.pickOrderStatus != "completed"} | |||
| onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)} | |||
| > | |||
| {t("Matching Stock")} | |||
| @@ -2,15 +2,19 @@ | |||
| import React, { useState, useEffect, useCallback } from "react"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; | |||
| import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; | |||
| import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | |||
| import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | |||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | |||
| import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | |||
| import { | |||
| fetchProductProcesses, | |||
| fetchProductProcessesByJobOrderId, | |||
| ProductProcessLineResponse | |||
| } from "@/app/api/jo/actions"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type PrinterCombo = { | |||
| id: number; | |||
| value: number; | |||
| @@ -25,17 +29,25 @@ type PrinterCombo = { | |||
| interface ProductionProcessPageProps { | |||
| printerCombo: PrinterCombo[]; | |||
| } | |||
| const STORAGE_KEY = 'productionProcess_selectedMatchingStock'; | |||
| const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | |||
| const { t } = useTranslation(["common"]); | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| const [selectedMatchingStock, setSelectedMatchingStock] = useState<{ | |||
| jobOrderId: number; | |||
| productProcessId: number; | |||
| } | null>(null); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // Add printer selection state | |||
| const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>( | |||
| printerCombo && printerCombo.length > 0 ? printerCombo[0] : null | |||
| ); | |||
| // 从 sessionStorage 恢复状态(仅在客户端) | |||
| useEffect(() => { | |||
| if (typeof window !== 'undefined') { | |||
| @@ -76,6 +88,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| } | |||
| }, []); | |||
| const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => { | |||
| setTabIndex(newValue); | |||
| }, []); | |||
| if (selectedMatchingStock) { | |||
| return ( | |||
| <JobPickExecutionsecondscan | |||
| @@ -84,6 +100,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| /> | |||
| ); | |||
| } | |||
| if (selectedProcessId !== null) { | |||
| return ( | |||
| <ProductionProcessJobOrderDetail | |||
| @@ -94,21 +111,86 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| } | |||
| return ( | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0 | |||
| }); | |||
| }} | |||
| /> | |||
| <Box> | |||
| {/* Header section with printer selection */} | |||
| {tabIndex === 1 && ( | |||
| <Box sx={{ | |||
| p: 1, | |||
| borderBottom: '1px solid #e0e0e0', | |||
| minHeight: 'auto', | |||
| display: 'flex', | |||
| alignItems: 'center', | |||
| justifyContent: 'flex-end', | |||
| gap: 2, | |||
| flexWrap: 'wrap', | |||
| }}> | |||
| <Stack | |||
| direction="row" | |||
| spacing={2} | |||
| sx={{ | |||
| alignItems: 'center', | |||
| flexWrap: 'wrap', | |||
| rowGap: 1, | |||
| }} | |||
| > | |||
| <Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo || []} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter || undefined} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 200 }} | |||
| size="small" | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| placeholder={t("Printer")} | |||
| inputProps={{ | |||
| ...params.inputProps, | |||
| readOnly: true, | |||
| }} | |||
| /> | |||
| )} | |||
| /> | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Finished QC Job Orders")} /> | |||
| </Tabs> | |||
| {tabIndex === 0 && ( | |||
| <ProductionProcessList | |||
| printerCombo={printerCombo} | |||
| onSelectProcess={(jobOrderId) => { | |||
| const id = jobOrderId ?? null; | |||
| if (id !== null) { | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0 | |||
| }); | |||
| }} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| <FinishedQcJobOrderList | |||
| printerCombo={printerCombo} | |||
| selectedPrinter={selectedPrinter} | |||
| /> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -19,6 +19,8 @@ import { | |||
| CardContent, | |||
| Grid, | |||
| } from "@mui/material"; | |||
| import { Alert } from "@mui/material"; | |||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import StopIcon from "@mui/icons-material/Stop"; | |||
| @@ -75,6 +77,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | |||
| const [isOverTime, setIsOverTime] = useState(false); | |||
| const [frozenRemainingTime, setFrozenRemainingTime] = useState<string | null>(null); | |||
| const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null); | |||
| const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | |||
| const [pauseReason, setPauseReason] = useState(""); | |||
| // 检查是否两个都已扫描 | |||
| @@ -91,7 +96,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| fetchProductProcessLineDetail(lineId) | |||
| .then((detail) => { | |||
| setLineDetail(detail as any); | |||
| // 初始化 outputData 从 lineDetail | |||
| console.log("📋 Line Detail loaded:", { | |||
| id: detail.id, | |||
| status: detail.status, | |||
| durationInMinutes: detail.durationInMinutes, | |||
| startTime: detail.startTime, | |||
| startTimeType: typeof detail.startTime, | |||
| hasDuration: !!detail.durationInMinutes, | |||
| hasStartTime: !!detail.startTime, | |||
| }); | |||
| setOutputData(prev => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| @@ -112,27 +125,124 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| }); | |||
| }, [lineId]); | |||
| useEffect(() => { | |||
| // Don't show time remaining if completed | |||
| if (lineDetail?.status === "Completed") { | |||
| console.log("Line is completed"); | |||
| setRemainingTime(null); | |||
| setIsOverTime(false); | |||
| return; | |||
| } | |||
| 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); | |||
| setIsOverTime(false); | |||
| 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 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; | |||
| } | |||
| 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(); | |||
| 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 () => { | |||
| if (!lineDetail?.id) return; | |||
| @@ -164,6 +274,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| fetchProductProcessLineDetail(lineDetail.id) | |||
| .then((detail) => { | |||
| console.log("Line Detail loaded:", { | |||
| id: detail.id, | |||
| status: detail.status, | |||
| startTime: detail.startTime, | |||
| durationInMinutes: detail.durationInMinutes, | |||
| productProcessIssueStatus: detail.productProcessIssueStatus | |||
| }); | |||
| setLineDetail(detail as any); | |||
| // 初始化 outputData 从 lineDetail | |||
| setOutputData(prev => ({ | |||
| @@ -249,6 +366,37 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| alert(t("Failed to pause. Please try again.")); | |||
| } | |||
| }; | |||
| // ✅ Add this new handler for resume | |||
| const handleResume = async () => { | |||
| if (!lineDetail?.productProcessIssueId) { | |||
| console.error("No productProcessIssueId found"); | |||
| return; | |||
| } | |||
| try { | |||
| await saveProductProcessResumeTime(lineDetail.productProcessIssueId); | |||
| console.log("✅ Resume API called successfully"); | |||
| // ✅ Refresh line detail after resume | |||
| if (lineDetail?.id) { | |||
| fetchProductProcessLineDetail(lineDetail.id) | |||
| .then((detail) => { | |||
| console.log("✅ Line detail refreshed after resume:", detail); | |||
| setLineDetail(detail as any); | |||
| // Clear frozen time when resuming | |||
| setFrozenRemainingTime(null); | |||
| setLastPauseTime(null); | |||
| }) | |||
| .catch(err => { | |||
| console.error("❌ Failed to load line detail after resume", err); | |||
| }); | |||
| } | |||
| } catch (error) { | |||
| console.error("❌ Error resuming:", error); | |||
| alert(t("Failed to resume. Please try again.")); | |||
| } | |||
| }; | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| @@ -256,7 +404,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* 如果已完成,显示合并的视图 */} | |||
| {isCompleted ? ( | |||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | |||
| @@ -426,7 +574,25 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {equipmentName} | |||
| </Typography> | |||
| {!isCompleted && remainingTime !== null && ( | |||
| <Box sx={{ mt: 2, mb: 2, p: 2, bgcolor: isOverTime ? 'error.50' : 'info.50', borderRadius: 1, border: '1px solid', borderColor: isOverTime ? 'error.main' : 'info.main' }}> | |||
| <Typography variant="body2" color="text.secondary" gutterBottom> | |||
| {t("Time Remaining")} | |||
| </Typography> | |||
| <Typography | |||
| variant="h5" | |||
| fontWeight="bold" | |||
| color={isOverTime ? 'error.main' : 'info.main'} | |||
| > | |||
| {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} | |||
| </Typography> | |||
| {lineDetail?.status === "Paused" && ( | |||
| <Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}> | |||
| {t("Timer Paused")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| )} | |||
| <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | |||
| {/* | |||
| <Button | |||
| @@ -453,7 +619,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| variant="contained" | |||
| color="success" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)} | |||
| onClick={handleResume} // ✅ Change from inline call to handler | |||
| > | |||
| {t("Continue")} | |||
| </Button> | |||
| @@ -462,6 +628,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Button | |||
| sx={{ mt: 2, alignSelf: "flex-end" }} | |||
| variant="outlined" | |||
| disabled={lineDetail?.status === 'Paused'} | |||
| onClick={() => setShowOutputTable(true)} | |||
| > | |||
| {t("Order Complete")} | |||
| @@ -521,39 +688,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* byproduct */} | |||
| {/* | |||
| <TableRow> | |||
| <TableCell> | |||
| <Stack> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductQty: parseInt(e.target.value) || 0 | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductUom} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| byproductUom: e.target.value | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| */} | |||
| {/* defect 1 */} | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| @@ -396,6 +396,52 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| // onOpenPutaway(); | |||
| const isJobOrderBom = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess") | |||
| && stockInLineInfo?.bomDescription === "半成品"; | |||
| 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"); | |||
| // setTabIndex(1); // Need to go Putaway tab? | |||
| } else { | |||
| @@ -1,5 +1,4 @@ | |||
| { | |||
| "dashboard": "資訊展示面板", | |||
| "Edit": "編輯", | |||
| "Job Order Production Process": "工單生產流程", | |||
| @@ -7,12 +6,28 @@ | |||
| "Search Criteria": "搜尋條件", | |||
| "All": "全部", | |||
| "No options": "沒有選項", | |||
| "Finished QC Job Orders": "完成QC工單", | |||
| "Reset": "重置", | |||
| "Search": "搜尋", | |||
| "Staff No Required": "員工編號必填", | |||
| "User Not Found": "用戶不存在", | |||
| "Time Remaining": "剩餘時間", | |||
| "Select Printer": "選擇打印機", | |||
| "Finished Time": "完成時間", | |||
| "Printer": "打印機", | |||
| "Finished Qc Job Order List": "完成QC工單列表", | |||
| "Total finished Qc Job Order": "總完成QC工單數量", | |||
| "Timer Paused": "計時器已暫停", | |||
| "User not found with staffNo:": "用戶不存在", | |||
| "Total finished QC job orders": "總完成QC工單數量", | |||
| "Over Time": "超時", | |||
| "Code": "編號", | |||
| "Staff No": "員工編號", | |||
| "code": "編號", | |||
| "Name": "名稱", | |||
| "Assignment successful": "分配成功", | |||
| "Pass": "通過", | |||
| "Unable to get user ID": "無法獲取用戶ID", | |||
| "Unknown error: ": "未知錯誤: ", | |||
| "Please try again later.": "請稍後重試。", | |||
| @@ -25,7 +40,6 @@ | |||
| "R&D": "研發", | |||
| "STF": "樣品", | |||
| "Other": "其他", | |||
| "Add some entries!": "添加條目", | |||
| "Add Record": "新增", | |||
| "Clean Record": "重置", | |||
| @@ -49,35 +63,35 @@ | |||
| "Changeover Time": "生產後轉換時間", | |||
| "Warehouse": "倉庫", | |||
| "Supplier": "供應商", | |||
| "Purchase Order":"採購單", | |||
| "Demand Forecast":"需求預測", | |||
| "Purchase Order": "採購單", | |||
| "Demand Forecast": "需求預測", | |||
| "Pick Order": "提料單", | |||
| "Deliver Order":"送貨訂單", | |||
| "Project":"專案", | |||
| "Product":"產品", | |||
| "Material":"材料", | |||
| "mat":"原料", | |||
| "Deliver Order": "送貨訂單", | |||
| "Project": "專案", | |||
| "Product": "產品", | |||
| "Material": "材料", | |||
| "mat": "原料", | |||
| "consumables": "消耗品", | |||
| "non-consumables": "非消耗品", | |||
| "fg": "成品", | |||
| "sfg": "半成品", | |||
| "item": "貨品", | |||
| "FG":"成品", | |||
| "Qty":"數量", | |||
| "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | |||
| "View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", | |||
| "Delivery Order":"送貨訂單", | |||
| "Detail Scheduling":"詳細排程", | |||
| "Customer":"客戶", | |||
| "qcItem":"品檢項目", | |||
| "Item":"物料", | |||
| "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": "總覽", | |||
| "Projects": "專案", | |||
| "Create Project": "新增專案", | |||
| @@ -86,21 +100,21 @@ | |||
| "Qc Item": "QC 項目", | |||
| "FG Production Schedule": "FG 生產排程", | |||
| "Inventory": "庫存", | |||
| "scheduling":"排程", | |||
| "scheduling": "排程", | |||
| "settings": "設定", | |||
| "items": "物料", | |||
| "edit":"編輯", | |||
| "Edit Equipment Type":"設備類型詳情", | |||
| "Edit Equipment":"設備詳情", | |||
| "equipmentType":"設備類型", | |||
| "Description":"描述", | |||
| "edit": "編輯", | |||
| "Edit Equipment Type": "設備類型詳情", | |||
| "Edit Equipment": "設備詳情", | |||
| "equipmentType": "設備類型", | |||
| "Description": "描述", | |||
| "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": "新增", | |||
| "hr": "小時", | |||
| "hrs": "小時", | |||
| @@ -123,7 +137,6 @@ | |||
| "Stop Scan": "停止掃碼", | |||
| "Scan Result": "掃碼結果", | |||
| "Expiry Date": "有效期", | |||
| "Pick Order Code": "提料單編號", | |||
| "Target Date": "需求日期", | |||
| "Lot Required Pick Qty": "批號需求數量", | |||
| @@ -133,8 +146,6 @@ | |||
| "No data available": "沒有資料", | |||
| "jodetail": "工單細節", | |||
| "Sign out": "登出", | |||
| "By-product": "副產品", | |||
| "Complete Step": "完成步驟", | |||
| "Defect": "不良品", | |||
| @@ -161,7 +172,6 @@ | |||
| "Output Qty": "輸出數量", | |||
| "Pending": "待處理", | |||
| "pending": "待處理", | |||
| "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | |||
| "Please scan operator code": "請掃描操作員編號", | |||
| "Please scan operator code first": "請先掃描操作員編號", | |||
| @@ -173,11 +183,10 @@ | |||
| "Setup Time (mins)": "生產前預備時間(分鐘)", | |||
| "Start": "開始", | |||
| "Start QR Scan": "開始掃碼", | |||
| "Status": "狀態", | |||
| "Status": "狀態", | |||
| "in_progress": "進行中", | |||
| "In_Progress": "進行中", | |||
| "inProgress": "進行中", | |||
| "Step Name": "名稱", | |||
| "Stop QR Scan": "停止掃碼", | |||
| "Submit & Start": "提交並開始", | |||
| @@ -188,7 +197,7 @@ | |||
| "Back": "返回", | |||
| "BoM Material": "物料清單", | |||
| "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 Name": "物料名稱", | |||
| "Job Order Info": "工單信息", | |||
| @@ -234,4 +243,4 @@ | |||
| "Lines with sufficient stock: ": "可提料項目數量: ", | |||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | |||
| "Total lines: ": "總數量:" | |||
| } | |||
| } | |||
| @@ -8,11 +8,19 @@ | |||
| "Code": "工單編號", | |||
| "Name": "成品/半成品名稱", | |||
| "Picked Qty": "已提料數量", | |||
| "Req. Qty": "需求數量", | |||
| "Confirm All": "確認所有", | |||
| "UoM": "銷售單位", | |||
| "No": "沒有", | |||
| "User not found with staffNo:": "用戶不存在", | |||
| "Time Remaining": "剩餘時間", | |||
| "Over Time": "超時", | |||
| "Staff No:": "員工編號:", | |||
| "Timer Paused": "計時器已暫停", | |||
| "Staff No Required": "員工編號必填", | |||
| "Staff No": "員工編號", | |||
| "Status": "工單狀態", | |||
| "Lot No.": "批號", | |||
| "Pass": "通過", | |||
| "Delete Job Order": "刪除工單", | |||
| "Bom": "半成品/成品編號", | |||
| "Release": "放單", | |||
| @@ -136,7 +144,7 @@ | |||
| "Confirm Lot Substitution": "確認批號替換", | |||
| "Processing...": "處理中", | |||
| "Complete Job Order Record": "已完成工單記錄", | |||
| "Back": "返回", | |||
| "Lot Details": "批號細節", | |||
| "No lot details available": "沒有批號細節", | |||
| "Second Scan Completed": "對料已完成", | |||
| @@ -366,7 +374,7 @@ | |||
| "Number of cartons": "箱數", | |||
| "You need to enter a number": "您需要輸入一個數字", | |||
| "Number must be at least 1": "數字必須至少為1", | |||
| "Confirm": "確認", | |||
| "Cancel": "取消", | |||
| "Print Pick Record": "打印板頭紙", | |||
| "Printed Successfully.": "成功列印", | |||