瀏覽代碼

update print qr code scan equipment

master
CANCERYS\kw093 3 天之前
父節點
當前提交
72737c0cb3
共有 19 個文件被更改,包括 909 次插入209 次删除
  1. +5
    -2
      src/app/(main)/jo/edit/page.tsx
  2. +1
    -0
      src/app/api/bom/index.ts
  3. +39
    -2
      src/app/api/jo/actions.ts
  4. +1
    -0
      src/app/api/stockIn/index.ts
  5. +149
    -29
      src/components/JoSearch/JoCreateFormModal.tsx
  6. +1
    -0
      src/components/JoSearch/JoSearch.tsx
  7. +9
    -2
      src/components/Jodetail/JoPickOrderList.tsx
  8. +35
    -9
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  9. +3
    -2
      src/components/Jodetail/completeJobOrderRecord.tsx
  10. +6
    -5
      src/components/Jodetail/newJobPickExecution.tsx
  11. +204
    -0
      src/components/ProductionProcess/FinishedQcJobOrderList.tsx
  12. +59
    -42
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  13. +2
    -2
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  14. +3
    -2
      src/components/ProductionProcess/ProductionProcessList.tsx
  15. +97
    -15
      src/components/ProductionProcess/ProductionProcessPage.tsx
  16. +184
    -49
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx
  17. +46
    -0
      src/components/Qc/QcStockInModal.tsx
  18. +54
    -45
      src/i18n/zh/common.json
  19. +11
    -3
      src/i18n/zh/jo.json

+ 5
- 2
src/app/(main)/jo/edit/page.tsx 查看文件

@@ -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();
}




+ 1
- 0
src/app/api/bom/index.ts 查看文件

@@ -8,6 +8,7 @@ export interface BomCombo {
label: string;
outputQty: number;
outputQtyUom: string;
description: string;
}

export const preloadBomCombo = (() => {


+ 39
- 2
src/app/api/jo/actions.ts 查看文件

@@ -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[]>(


+ 1
- 0
src/app/api/stockIn/index.ts 查看文件

@@ -128,6 +128,7 @@ export interface StockInLine {
dnNo?: string;
dnDate?: number[];
stockQty?: number;
bomDescription?: string;
handlerId?: number;
putAwayLines?: PutAwayLine[];
qcResult?: QcResult[];


+ 149
- 29
src/components/JoSearch/JoCreateFormModal.tsx 查看文件

@@ -8,14 +8,16 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { isFinite } from "lodash";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react";
import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { msg } from "../Swal/CustomAlerts";
import { JobTypeResponse } from "@/app/api/jo/actions";

interface Props {
open: boolean;
bomCombo: BomCombo[];
jobTypes: JobTypeResponse[];
onClose: () => void;
onSearch: () => void;
}
@@ -23,6 +25,7 @@ interface Props {
const JoCreateFormModal: React.FC<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}


+ 1
- 0
src/components/JoSearch/JoSearch.tsx 查看文件

@@ -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 }); // 创建新对象,确保引用变化


+ 9
- 2
src/components/Jodetail/JoPickOrderList.tsx 查看文件

@@ -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>
);
}


+ 35
- 9
src/components/Jodetail/JobPickExecutionsecondscan.tsx 查看文件

@@ -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


+ 3
- 2
src/components/Jodetail/completeJobOrderRecord.tsx 查看文件

@@ -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',
},


+ 6
- 5
src/components/Jodetail/newJobPickExecution.tsx 查看文件

@@ -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(() => {


+ 204
- 0
src/components/ProductionProcess/FinishedQcJobOrderList.tsx 查看文件

@@ -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;

+ 59
- 42
src/components/ProductionProcess/ProductionProcessDetail.tsx 查看文件

@@ -33,16 +33,11 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchProductProcessById,
updateProductProcessLineQrscan,
// updateProductProcessLineQrscan,
newUpdateProductProcessLineQrscan,
fetchProductProcessLineDetail,
ProductProcessLineDetailResponse,
JobOrderProcessLineDetailResponse,
updateLineOutput,
ProductProcessLineInfoResponse,
ProductProcessResponse,
ProductProcessLineResponse,
completeProductProcessLine,
startProductProcessLine,
fetchProductProcessesByJobOrderId
} from "@/app/api/jo/actions";
@@ -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>


+ 2
- 2
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx 查看文件

@@ -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;


+ 3
- 2
src/components/ProductionProcess/ProductionProcessList.tsx 查看文件

@@ -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")}


+ 97
- 15
src/components/ProductionProcess/ProductionProcessPage.tsx 查看文件

@@ -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>
);
};


+ 184
- 49
src/components/ProductionProcess/ProductionProcessStepExecution.tsx 查看文件

@@ -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>


+ 46
- 0
src/components/Qc/QcStockInModal.tsx 查看文件

@@ -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 {


+ 54
- 45
src/i18n/zh/common.json 查看文件

@@ -1,5 +1,4 @@
{

"dashboard": "資訊展示面板",
"Edit": "編輯",
"Job Order Production Process": "工單生產流程",
@@ -7,12 +6,28 @@
"Search Criteria": "搜尋條件",
"All": "全部",
"No options": "沒有選項",
"Finished QC Job Orders": "完成QC工單",
"Reset": "重置",
"Search": "搜尋",
"Staff No Required": "員工編號必填",
"User Not Found": "用戶不存在",
"Time Remaining": "剩餘時間",
"Select Printer": "選擇打印機",
"Finished Time": "完成時間",
"Printer": "打印機",
"Finished Qc Job Order List": "完成QC工單列表",
"Total finished Qc Job Order": "總完成QC工單數量",
"Timer Paused": "計時器已暫停",
"User not found with staffNo:": "用戶不存在",
"Total finished QC job orders": "總完成QC工單數量",
"Over Time": "超時",
"Code": "編號",
"Staff No": "員工編號",
"code": "編號",
"Name": "名稱",
"Assignment successful": "分配成功",
"Pass": "通過",
"Unable to get user ID": "無法獲取用戶ID",
"Unknown error: ": "未知錯誤: ",
"Please try again later.": "請稍後重試。",
@@ -25,7 +40,6 @@
"R&D": "研發",
"STF": "樣品",
"Other": "其他",

"Add some entries!": "添加條目",
"Add Record": "新增",
"Clean Record": "重置",
@@ -49,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: ": "總數量:"
}
}

+ 11
- 3
src/i18n/zh/jo.json 查看文件

@@ -8,11 +8,19 @@
"Code": "工單編號",
"Name": "成品/半成品名稱",
"Picked Qty": "已提料數量",
"Req. Qty": "需求數量",
"Confirm All": "確認所有",
"UoM": "銷售單位",
"No": "沒有",
"User not found with staffNo:": "用戶不存在",
"Time Remaining": "剩餘時間",
"Over Time": "超時",
"Staff No:": "員工編號:",
"Timer Paused": "計時器已暫停",
"Staff No Required": "員工編號必填",
"Staff No": "員工編號",
"Status": "工單狀態",
"Lot No.": "批號",
"Pass": "通過",
"Delete Job Order": "刪除工單",
"Bom": "半成品/成品編號",
"Release": "放單",
@@ -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.": "成功列印",


Loading…
取消
儲存