| @@ -7,6 +7,7 @@ export interface BomCombo { | |||
| value: number; | |||
| label: string; | |||
| outputQty: number; | |||
| outputQtyUom: string; | |||
| } | |||
| export const preloadBomCombo = (() => { | |||
| @@ -248,6 +248,8 @@ export interface ProductProcessWithLinesResponse { | |||
| isDark: string; | |||
| isDense: number; | |||
| isFloat: string; | |||
| timeSequence: number; | |||
| complexity: number; | |||
| scrapRate: number; | |||
| allergicSubstance: string; | |||
| itemId: number; | |||
| @@ -315,6 +317,8 @@ export interface AllJoborderProductProcessInfoResponse { | |||
| endTime?: string; | |||
| date: string; | |||
| bomId?: number; | |||
| assignedTo: number; | |||
| pickOrderId: number; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| jobOrderId: number; | |||
| @@ -834,7 +838,15 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe | |||
| } | |||
| ); | |||
| }; | |||
| export const unAssignJobOrderPickOrder = async (pickOrderId: number) => { | |||
| return serverFetchJson<AssignJobOrderResponse>( | |||
| `${BASE_API_URL}/jo/unassign-job-order-pick-order/${pickOrderId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| } | |||
| ); | |||
| }; | |||
| // 获取 Job Order 分层数据 | |||
| export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||
| @@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo"; | |||
| import { SaveJo, manualCreateJo } from "@/app/api/jo/actions"; | |||
| import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import { Check } from "@mui/icons-material"; | |||
| import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem} from "@mui/material"; | |||
| import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material"; | |||
| import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| @@ -66,7 +66,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| msg(t("update success")); | |||
| onModalClose(); | |||
| } | |||
| }, []) | |||
| }, [onSearch, onModalClose, t]) | |||
| const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => { | |||
| console.log(error) | |||
| @@ -166,25 +166,36 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| required: "Req. Qty. required!", | |||
| validate: (value) => value > 0 | |||
| }} | |||
| render={({ field, fieldState: { error } }) => ( | |||
| <TextField | |||
| {...field} | |||
| label={t("Req. Qty")} | |||
| fullWidth | |||
| error={Boolean(error)} | |||
| variant="outlined" | |||
| type="number" | |||
| disabled={true} | |||
| // sx={{ | |||
| // backgroundColor: "background.paper", | |||
| // }} | |||
| value={field.value ?? ""} | |||
| onChange={(e) => { | |||
| const val = e.target.value === "" ? undefined : Number(e.target.value); | |||
| field.onChange(val); | |||
| }} | |||
| /> | |||
| )} | |||
| render={({ field, fieldState: { error } }) => { | |||
| const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId")); | |||
| const uom = selectedBom?.outputQtyUom || ""; | |||
| return ( | |||
| <TextField | |||
| {...field} | |||
| label={t("Req. Qty")} | |||
| fullWidth | |||
| error={Boolean(error)} | |||
| variant="outlined" | |||
| type="number" | |||
| disabled={true} | |||
| value={field.value ?? ""} | |||
| onChange={(e) => { | |||
| const val = e.target.value === "" ? undefined : Number(e.target.value); | |||
| field.onChange(val); | |||
| }} | |||
| InputProps={{ | |||
| endAdornment: uom ? ( | |||
| <InputAdornment position="end"> | |||
| <Typography variant="body2" sx={{ color: "text.secondary" }}> | |||
| {uom} | |||
| </Typography> | |||
| </InputAdornment> | |||
| ) : null | |||
| }} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={6}> | |||
| @@ -400,7 +400,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| bomCombo={bomCombo} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={() => { | |||
| setInputs(defaultInputs); | |||
| setPagingController(defaultPagingController); | |||
| }} | |||
| /> | |||
| @@ -10,9 +10,15 @@ interface SubComponents { | |||
| } | |||
| const JoSearchWrapper: React.FC & SubComponents = async () => { | |||
| const today = new Date(); | |||
| const todayStr = today.toISOString().split('T')[0]; | |||
| const defaultInputs: SearchJoResultRequest = { | |||
| code: "", | |||
| itemName: "", | |||
| planStart: `${todayStr}T00:00`, | |||
| planStartTo: `${todayStr}T23:59:59`, | |||
| } | |||
| const [ | |||
| @@ -1007,7 +1007,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||
| showInputBody={showInputBody} | |||
| onIssueNoLotStockOutLine={handleIssueNoLotStockOutLine} | |||
| setShowInputBody={setShowInputBody} | |||
| selectedLotForInput={selectedLotForInput} | |||
| //selectedLotForInput={selectedLotForInput} | |||
| generateInputBody={generateInputBody} | |||
| // Add missing props | |||
| totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed | |||
| @@ -10,6 +10,7 @@ interface Props { | |||
| itemName?: string; | |||
| jobType?: string; | |||
| outputQty?: number | string; | |||
| outputQtyUom?: string; | |||
| date?: string; | |||
| }; | |||
| } | |||
| @@ -32,7 +33,7 @@ const ProcessSummaryHeader: React.FC<Props> = ({ processData }) => { | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Qty")}: <strong style={{ color: "green" }}>{processData?.outputQty}</strong> | |||
| {t("Qty")}: <strong style={{ color: "green" }}>{processData?.outputQty}</strong> ({processData?.outputQtyUom ?? ""}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Production Date")}: <strong style={{ color: "green" }}>{processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}</strong> | |||
| @@ -599,12 +599,14 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Time Information(mins)")} | |||
| </Typography> | |||
| {/* | |||
| <Typography variant="caption" sx={{ color: 'text.secondary' }}> | |||
| {t("Processing Time")}- | |||
| </Typography> | |||
| <Typography variant="caption" sx={{ color: 'text.secondary' }}> | |||
| {t("Setup Time")} - {t("Changeover Time")} | |||
| </Typography> | |||
| */} | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell align="center">{t("Status")}</TableCell> | |||
| @@ -637,7 +639,20 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} </Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} </Typography></TableCell> | |||
| */} | |||
| <TableCell><Typography fontWeight={500}>{line.durationInMinutes} - {line.prepTimeInMinutes} - {line.postProdTimeInMinutes} </Typography></TableCell> | |||
| <TableCell> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography variant="body2" > | |||
| {t("Processing Time")}: {line.durationInMinutes}{t("mins")} | |||
| </Typography> | |||
| <Typography variant="body2" > | |||
| {t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")} | |||
| </Typography> | |||
| <Typography variant="body2" > | |||
| {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {isCompleted ? ( | |||
| <Chip label={t("Completed")} color="success" size="small" | |||
| @@ -38,6 +38,7 @@ import { releaseJo, startJo } from "@/app/api/jo/actions"; | |||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | |||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | |||
| import EditIcon from "@mui/icons-material/Edit"; | |||
| interface JobOrderLine { | |||
| id: number; | |||
| jobOrderId: number; | |||
| @@ -292,10 +293,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance")} | |||
| label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={`${processData?.isDark == null || processData?.isDark === "" ? t("N/A") : processData.isDark} | ${processData?.isDense == null || processData?.isDense === "" || processData?.isDense === 0 ? t("N/A") : processData.isDense} | ${processData?.isFloat == null || processData?.isFloat === "" ? t("N/A") : processData.isFloat} | ${processData?.scrapRate == -1 || processData?.scrapRate === "" ? t("N/A") : processData.scrapRate} | ${processData?.allergicSubstance == null || processData?.allergicSubstance === "" ? t("N/A") :t (processData.allergicSubstance)}`} | |||
| value={`${processData?.isDark == null || processData?.isDark === "" ? t("N/A") : processData.isDark} | ${processData?.isDense == null || processData?.isDense === "" || processData?.isDense === 0 ? t("N/A") : processData.isDense} | ${processData?.isFloat == null || processData?.isFloat === "" ? t("N/A") : processData.isFloat} | ${processData?.scrapRate == -1 || processData?.scrapRate === "" ? t("N/A") : processData.scrapRate} | ${processData?.allergicSubstance == null || processData?.allergicSubstance === "" ? t("N/A") :t (processData.allergicSubstance)} | ${processData?.timeSequence == null || processData?.timeSequence === "" ? t("N/A") : processData.timeSequence} | ${processData?.complexity == null || processData?.complexity === "" ? t("N/A") : processData.complexity}`} | |||
| /> | |||
| </Grid> | |||
| @@ -311,10 +312,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| headerName: t("Seq"), | |||
| flex: 0.2, | |||
| align: "left", | |||
| headerAlign: "center", | |||
| headerAlign: "left", | |||
| type: "number", | |||
| renderCell: (params) => { | |||
| return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>; | |||
| return <Typography sx={{ fontSize: "18px" }}>{params.value}</Typography>; | |||
| }, | |||
| }, | |||
| { | |||
| @@ -322,9 +323,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| headerName: t("Remark"), | |||
| flex: 1, | |||
| align: "left", | |||
| headerAlign: "center", | |||
| headerAlign: "left", | |||
| renderCell: (params) => { | |||
| return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>; | |||
| return <Typography sx={{ fontSize: "18px" }}>{params.value || ""}</Typography>; | |||
| }, | |||
| }, | |||
| ]; | |||
| @@ -521,9 +522,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| <Tab label={t("Production Process Line Remark")} /> | |||
| {!fromJosave && ( | |||
| {/* {!fromJosave && ( | |||
| <Tab label={t("Matching Stock")} /> | |||
| )} | |||
| )} */} | |||
| </Tabs> | |||
| </Box> | |||
| @@ -545,8 +546,8 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| )} | |||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | |||
| {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} | |||
| {/* {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} */} | |||
| <Dialog | |||
| open={openOperationPriorityDialog} | |||
| onClose={handleClosePriorityDialog} | |||
| @@ -24,19 +24,22 @@ import { | |||
| AllJoborderProductProcessInfoResponse, | |||
| updateJo, | |||
| fetchProductProcessesByJobOrderId, | |||
| completeProductProcessLine | |||
| completeProductProcessLine, | |||
| assignJobOrderPickOrder | |||
| } from "@/app/api/jo/actions"; | |||
| import { StockInLineInput } from "@/app/api/stockIn"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | |||
| interface ProductProcessListProps { | |||
| onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; | |||
| onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; | |||
| printerCombo: PrinterCombo[]; | |||
| } | |||
| const PER_PAGE = 6; | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess, printerCombo }) => { | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess, printerCombo ,onSelectMatchingStock}) => { | |||
| const { t } = useTranslation( ["common", "production","purchaseOrder"]); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const sessionToken = session as SessionWithTokens | null; | |||
| @@ -45,6 +48,44 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| const [page, setPage] = useState(0); | |||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => { | |||
| if (!currentUserId) { | |||
| alert(t("无法获取用户ID")); | |||
| return; | |||
| } | |||
| try { | |||
| console.log("🔄 Assigning pick order:", pickOrderId, "to user:", currentUserId); | |||
| // 调用分配 API 并读取响应 | |||
| const assignResult = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||
| console.log("📦 Assign result:", assignResult); | |||
| // 检查分配是否成功 | |||
| if (assignResult.message === "Successfully assigned") { | |||
| console.log("✅ Successfully assigned pick order"); | |||
| console.log("✅ Pick order ID:", assignResult.id); | |||
| console.log("✅ Pick order code:", assignResult.code); | |||
| // 分配成功后,导航到 second scan 页面 | |||
| if (onSelectMatchingStock && jobOrderId) { | |||
| onSelectMatchingStock(jobOrderId, productProcessId); | |||
| } else { | |||
| alert(t("分配成功")); | |||
| } | |||
| } else { | |||
| // 分配失败 | |||
| console.error("❌ Assignment failed:", assignResult.message); | |||
| alert(t(`分配失败: ${assignResult.message || "未知错误"}`)); | |||
| } | |||
| } catch (error: any) { | |||
| console.error("❌ Error assigning pick order:", error); | |||
| alert(t(`分配时出错: ${error?.message || "未知错误"}。请稍后重试。`)); | |||
| } | |||
| }, [currentUserId, t, onSelectMatchingStock]); | |||
| const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => { | |||
| if (!process.stockInLineId) { | |||
| alert(t("Invalid Stock In Line Id")); | |||
| @@ -224,6 +265,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}> | |||
| {t("View Details")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| disabled={process.assignedTo != null} | |||
| onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)} | |||
| > | |||
| {t("Matching Stock")} | |||
| </Button> | |||
| {statusLower !== "completed" && ( | |||
| <Button variant="contained" size="small" onClick={() => handleUpdateJo(process)}> | |||
| {t("Update Job Order")} | |||
| @@ -5,7 +5,7 @@ import { SessionWithTokens } from "@/config/authConfig"; | |||
| 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 { | |||
| fetchProductProcesses, | |||
| fetchProductProcessesByJobOrderId, | |||
| @@ -28,11 +28,21 @@ interface ProductionProcessPageProps { | |||
| const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => { | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| const [selectedMatchingStock, setSelectedMatchingStock] = useState<{ | |||
| jobOrderId: number; | |||
| productProcessId: number; | |||
| } | null>(null); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // …原有逻辑省略… | |||
| if (selectedMatchingStock) { | |||
| return ( | |||
| <JobPickExecutionsecondscan | |||
| filterArgs={{ jobOrderId: selectedMatchingStock.jobOrderId }} | |||
| onBack={() => setSelectedMatchingStock(null)} | |||
| /> | |||
| ); | |||
| } | |||
| if (selectedProcessId !== null) { | |||
| return ( | |||
| <ProductionProcessJobOrderDetail | |||
| @@ -51,6 +61,12 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| setSelectedProcessId(id); | |||
| } | |||
| }} | |||
| onSelectMatchingStock={(jobOrderId, productProcessId) => { | |||
| setSelectedMatchingStock({ | |||
| jobOrderId: jobOrderId || 0, | |||
| productProcessId: productProcessId || 0 | |||
| }); | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -118,7 +118,7 @@ | |||
| "Stop Scan": "停止掃碼", | |||
| "Scan Result": "掃碼結果", | |||
| "Expiry Date": "有效期", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原", | |||
| "Pick Order Code": "提料單編號", | |||
| "Target Date": "需求日期", | |||
| "Lot Required Pick Qty": "批號需求數量", | |||
| @@ -183,7 +183,7 @@ | |||
| "Back": "返回", | |||
| "BoM Material": "物料清單", | |||
| "N/A": "不適用", | |||
| "Is Dark | Dense | Float | Scrap Rate | Allergic Substance": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | |||
| "Item Code": "物料編號", | |||
| "Item Name": "物料名稱", | |||
| "Job Order Info": "工單信息", | |||
| @@ -210,6 +210,7 @@ | |||
| "Reason": "原因", | |||
| "Invalid Stock In Line Id": "無效庫存行ID", | |||
| "Production date": "生產日期", | |||
| "update production priority": "更新生產優先級", | |||
| "Required Qty": "需求數量", | |||
| "Total processes": "總流程數", | |||
| "View Details": "查看詳情", | |||
| @@ -47,6 +47,7 @@ | |||
| "Pause Reason": "暫停原因", | |||
| "Reason": "原因", | |||
| "Stock Available": "倉庫可用數", | |||
| "update production priority": "更新生產優先級", | |||
| "Staff No": "員工編號", | |||
| "Please scan staff no": "請掃描員工編號", | |||
| "Stock Status": "可提料", | |||
| @@ -349,8 +350,8 @@ | |||
| "View": "查看", | |||
| "Back": "返回", | |||
| "N/A": "不適用", | |||
| "BoM Material": "半成品/成品清單", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原", | |||
| "BoM Material": "物料清單", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | |||
| "Item Code": "物料編號", | |||
| "Item Name": "物料名稱", | |||
| "Enter the number of cartons: ": "請輸入箱數:", | |||