| @@ -1,5 +1,5 @@ | |||
| import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage"; | |||
| import { getServerI18n } from "../../../i18n"; | |||
| import { I18nProvider, getServerI18n } from "../../../i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| import Button from "@mui/material/Button"; | |||
| @@ -15,7 +15,7 @@ export const metadata: Metadata = { | |||
| }; | |||
| const production: React.FC = async () => { | |||
| const { t } = await getServerI18n("claims"); | |||
| const { t } = await getServerI18n("common"); | |||
| const printerCombo = await fetchPrinterCombo(); | |||
| return ( | |||
| <> | |||
| @@ -38,7 +38,9 @@ const production: React.FC = async () => { | |||
| {t("Create Process")} | |||
| </Button> */} | |||
| </Stack> | |||
| <ProductionProcessPage printerCombo={printerCombo} /> {/* Use new component */} | |||
| <I18nProvider namespaces={["common", "production","purchaseOrder"]}> | |||
| <ProductionProcessPage printerCombo={printerCombo} /> {/* Use new component */} | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -263,6 +263,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||
| date: string; | |||
| bomId?: number; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| jobOrderId: number; | |||
| stockInLineId: number; | |||
| jobOrderCode: string; | |||
| @@ -332,6 +333,7 @@ export interface JobOrderProcessLineDetailResponse { | |||
| operatorName: string; | |||
| handlerId: number; | |||
| seqNo: number; | |||
| durationInMinutes: number; | |||
| name: string; | |||
| description: string; | |||
| equipmentId: number; | |||
| @@ -360,7 +362,10 @@ export interface JobOrderLineInfo { | |||
| stockQty: number, | |||
| uom: string, | |||
| shortUom: string, | |||
| availableStatus: string | |||
| availableStatus: string, | |||
| bomProcessId: number, | |||
| bomProcessSeqNo: number, | |||
| } | |||
| export interface ProductProcessLineInfoResponse { | |||
| id: number, | |||
| @@ -0,0 +1,210 @@ | |||
| "use client"; | |||
| import React from "react"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Paper, | |||
| Stack, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableHead, | |||
| TableRow, | |||
| TextField, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { UpdateProductProcessLineQtyRequest } from "@/app/api/jo/actions"; | |||
| interface ProductionOutputFormProps { | |||
| outputData: (UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| }); | |||
| setOutputData: React.Dispatch< | |||
| React.SetStateAction< | |||
| UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| } | |||
| > | |||
| >; | |||
| onSubmit: () => Promise<void> | void; | |||
| onCancel: () => void; | |||
| } | |||
| const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| outputData, | |||
| setOutputData, | |||
| onSubmit, | |||
| onCancel, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Paper sx={{ p: 3, bgcolor: "grey.50" }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell width="30%">{t("Type")}</TableCell> | |||
| <TableCell width="35%">{t("Quantity")}</TableCell> | |||
| <TableCell width="35%">{t("Unit")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessQty} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessQty: parseInt(e.target.value) || 0, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessUom} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessUom: e.target.value, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| <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> | |||
| <TableRow sx={{ bgcolor: "warning.50" }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="warning.dark"> | |||
| {t("Defect")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectQty} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| defectQty: parseInt(e.target.value) || 0, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectUom} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| defectUom: e.target.value, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| <TableRow sx={{ bgcolor: "error.50" }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="error.dark"> | |||
| {t("Scrap")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapQty} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| scrapQty: parseInt(e.target.value) || 0, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapUom} | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| scrapUom: e.target.value, | |||
| }) | |||
| } | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| <Box sx={{ mt: 3, display: "flex", gap: 2 }}> | |||
| <Button variant="outlined" onClick={onCancel}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button variant="contained" startIcon={<CheckCircleIcon />} onClick={onSubmit}> | |||
| {t("Complete Step")} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default ProductionOutputForm; | |||
| @@ -0,0 +1,146 @@ | |||
| "use client"; | |||
| import React, { useEffect, useState } from "react"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| fetchProductProcessLineDetail, | |||
| updateProductProcessLineQty, | |||
| UpdateProductProcessLineQtyRequest, | |||
| JobOrderProcessLineDetailResponse, | |||
| } from "@/app/api/jo/actions"; | |||
| import ProductionOutputForm from "./ProductionOutputForm"; | |||
| interface ProductionOutputFormPageProps { | |||
| lineId: number | null; | |||
| onBack: () => void; | |||
| } | |||
| const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| lineId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| }>({ | |||
| productProcessLineId: lineId ?? 0, | |||
| outputFromProcessQty: 0, | |||
| outputFromProcessUom: "", | |||
| defectQty: 0, | |||
| defectUom: "", | |||
| scrapQty: 0, | |||
| scrapUom: "", | |||
| byproductName: "", | |||
| byproductQty: 0, | |||
| byproductUom: "", | |||
| }); | |||
| useEffect(() => { | |||
| if (!lineId) { | |||
| setLineDetail(null); | |||
| return; | |||
| } | |||
| fetchProductProcessLineDetail(lineId) | |||
| .then((detail) => { | |||
| setLineDetail(detail as any); | |||
| setOutputData((prev) => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectUom: detail.defectUom || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductUom: detail.byproductUom || "", | |||
| })); | |||
| }) | |||
| .catch((err) => { | |||
| console.error("Failed to load line detail", err); | |||
| setLineDetail(null); | |||
| }); | |||
| }, [lineId]); | |||
| const handleSubmitOutput = async () => { | |||
| if (!lineDetail?.id) return; | |||
| try { | |||
| await updateProductProcessLineQty({ | |||
| productProcessLineId: lineDetail.id || 0, | |||
| byproductName: outputData.byproductName, | |||
| byproductQty: outputData.byproductQty, | |||
| byproductUom: outputData.byproductUom, | |||
| outputFromProcessQty: outputData.outputFromProcessQty, | |||
| outputFromProcessUom: outputData.outputFromProcessUom, | |||
| defectQty: outputData.defectQty, | |||
| defectUom: outputData.defectUom, | |||
| scrapQty: outputData.scrapQty, | |||
| scrapUom: outputData.scrapUom, | |||
| }); | |||
| console.log("Output data submitted successfully"); | |||
| // 重新加载数据 | |||
| const detail = await fetchProductProcessLineDetail(lineDetail.id); | |||
| setLineDetail(detail as any); | |||
| setOutputData((prev) => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectUom: detail.defectUom || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductUom: detail.byproductUom || "", | |||
| })); | |||
| // 提交成功后返回 | |||
| onBack(); | |||
| } catch (error) { | |||
| console.error("Error submitting output:", error); | |||
| alert("Failed to submit output data. Please try again."); | |||
| } | |||
| }; | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| <Box sx={{ mb: 3 }}> | |||
| <Typography variant="h5" gutterBottom fontWeight="bold"> | |||
| {t("Production Output Data Entry")} | |||
| </Typography> | |||
| {lineDetail && ( | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Step")}: {lineDetail.name} (Seq: {lineDetail.seqNo}) | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| <ProductionOutputForm | |||
| outputData={outputData} | |||
| setOutputData={setOutputData} | |||
| onSubmit={handleSubmitOutput} | |||
| onCancel={onBack} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ProductionOutputFormPage; | |||
| @@ -48,7 +48,7 @@ import { | |||
| } from "@/app/api/jo/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; | |||
| import ProductionOutputFormPage from "./ProductionOutputFormPage"; | |||
| interface ProductProcessDetailProps { | |||
| jobOrderId: number; | |||
| @@ -63,7 +63,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const [showOutputPage, setShowOutputPage] = useState(false); | |||
| // 基本信息 | |||
| const [processData, setProcessData] = useState<any>(null); | |||
| const [lines, setLines] = useState<ProductProcessLineInfoResponse[]>([]); | |||
| @@ -102,6 +102,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| await fetchProcessDetail(); // 重新拉取最新的 process/lines | |||
| setIsExecutingLine(false); | |||
| setSelectedLineId(null); | |||
| setShowOutputPage(false); | |||
| }; | |||
| // 获取 process 和 lines 数据 | |||
| @@ -486,11 +487,12 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| <TableCell>{t("Seq")}</TableCell> | |||
| <TableCell>{t("Step Name")}</TableCell> | |||
| <TableCell>{t("Description")}</TableCell> | |||
| <TableCell>{t("Equipment Type/Code")}</TableCell> | |||
| <TableCell>{t("Operator")}</TableCell> | |||
| <TableCell>{t("Equipment Type")}</TableCell> | |||
| <TableCell>{t("Duration")}</TableCell> | |||
| <TableCell>{t("Prep Time")}</TableCell> | |||
| <TableCell>{t("Post Prod Time")}</TableCell> | |||
| <TableCell>{t("Processing Time (mins)")}</TableCell> | |||
| <TableCell>{t("Setup Time (mins)")}</TableCell> | |||
| <TableCell>{t("Changeover Time (mins)")}</TableCell> | |||
| <TableCell align="center">{t("Status")}</TableCell> | |||
| <TableCell align="center">{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -512,16 +514,30 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| <Typography fontWeight={500}>{line.name}</Typography> | |||
| </TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{equipmentName}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.durationInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} {t("Minutes")}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.durationInMinutes} </Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} </Typography></TableCell> | |||
| <TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} </Typography></TableCell> | |||
| <TableCell align="center"> | |||
| {isCompleted ? ( | |||
| <Chip label={t("Completed")} color="success" size="small" /> | |||
| <Chip label={t("Completed")} color="success" size="small" | |||
| onClick={async () => { | |||
| setSelectedLineId(line.id); | |||
| setShowOutputPage(false); // 不显示输出页面 | |||
| setIsExecutingLine(true); | |||
| await fetchProcessDetail(); | |||
| }} | |||
| /> | |||
| ) : isInProgress ? ( | |||
| <Chip label={t("In Progress")} color="primary" size="small" /> | |||
| <Chip label={t("In Progress")} color="primary" size="small" | |||
| onClick={async () => { | |||
| setSelectedLineId(line.id); | |||
| setShowOutputPage(false); // 不显示输出页面 | |||
| setIsExecutingLine(true); | |||
| await fetchProcessDetail(); | |||
| }} /> | |||
| ) : isPending ? ( | |||
| <Chip label={t("Pending")} color="default" size="small" /> | |||
| ) : ( | |||
| @@ -1,5 +1,5 @@ | |||
| "use client"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import React, { useCallback, useEffect, useState, useMemo } from "react"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -99,7 +99,36 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| useEffect(() => { | |||
| fetchData(); | |||
| }, [fetchData]); | |||
| // PickTable 组件内容 | |||
| const getStockAvailable = (line: JobOrderLine) => { | |||
| const inventory = inventoryData.find(inv => | |||
| inv.itemCode === line.itemCode || inv.itemName === line.itemName | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return line.stockQty || 0; | |||
| }; | |||
| const isStockSufficient = (line: JobOrderLine) => { | |||
| const stockAvailable = getStockAvailable(line); | |||
| return stockAvailable >= line.reqQty; | |||
| }; | |||
| const stockCounts = useMemo(() => { | |||
| const total = jobOrderLines.length; | |||
| const sufficient = jobOrderLines.filter(isStockSufficient).length; | |||
| return { | |||
| total, | |||
| sufficient, | |||
| insufficient: total - sufficient, | |||
| }; | |||
| }, [jobOrderLines, inventoryData]); | |||
| const status = processData?.status?.toLowerCase?.() ?? ""; | |||
| const handleRelease = useCallback(() => { | |||
| // TODO: 替换为实际的 release 调用 | |||
| console.log("Release clicked for jobOrderId:", jobOrderId); | |||
| }, [jobOrderId]); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| @@ -139,6 +168,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| ); | |||
| } | |||
| // InfoCard 组件内容 | |||
| const InfoCardContent = () => ( | |||
| <Card sx={{ display: "block", mt: 2 }}> | |||
| @@ -153,21 +183,13 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| value={processData?.jobOrderCode || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}/> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Item Code")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.itemCode || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Item Name")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.itemName || ""} | |||
| value={processData?.itemCode+"-"+processData?.itemName || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -175,17 +197,10 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| label={t("Req. Qty")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.outputQty ? integerFormatter.format(processData.outputQty) : ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("UoM")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.outputQtyUom || ""} | |||
| value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} | |||
| @@ -204,49 +219,19 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Dark")} | |||
| label={t("Is Dark | Dense | Float")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isDark || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Dense")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isDense || ""} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Is Float")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.isFloat || ""} | |||
| value={processData?.isDark + " | " + processData?.isDense + " | " + processData?.isFloat || ""} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| // PickTable 组件内容 | |||
| const getStockAvailable = (line: JobOrderLine) => { | |||
| const inventory = inventoryData.find(inv => | |||
| inv.itemCode === line.itemCode || inv.itemName === line.itemName | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return line.stockQty || 0; | |||
| }; | |||
| const isStockSufficient = (line: JobOrderLine) => { | |||
| const stockAvailable = getStockAvailable(line); | |||
| return stockAvailable >= line.reqQty; | |||
| }; | |||
| const productionProcessesLineRemarkTableColumns: GridColDef[] = [ | |||
| { | |||
| field: "seqNo", | |||
| @@ -321,11 +306,28 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| }, | |||
| }, | |||
| { | |||
| field: "stockStatus", | |||
| headerName: t("Stock Status"), | |||
| field: "bomProcessSeqNo", | |||
| headerName: t("Seq No"), | |||
| flex: 0.5, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| type: "number", | |||
| }, | |||
| { | |||
| field: "seqNoRemark", | |||
| headerName: t("Seq No Remark"), | |||
| flex: 1, | |||
| align: "left", | |||
| headerAlign: "left", | |||
| type: "string", | |||
| }, | |||
| { | |||
| field: "stockStatus", | |||
| headerName: t("Stock Status"), | |||
| flex: 0.5, | |||
| align: "center", | |||
| headerAlign: "center", | |||
| type: "boolean", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| return isStockSufficient(params.row) | |||
| @@ -342,14 +344,44 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| const PickTableContent = () => ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <Stack | |||
| direction="row" | |||
| alignItems="center" | |||
| justifyContent="space-between" | |||
| spacing={2} | |||
| > | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Total lines: ")}<strong>{stockCounts.total}</strong> | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong> | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong> | |||
| </Typography> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={handleRelease} | |||
| disabled={stockCounts.insufficient > 0 || status !== "planning"} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| <StyledDataGrid | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| }} | |||
| sx={{ "--DataGrid-overlayHeight": "100px" }} | |||
| disableColumnMenu | |||
| rows={pickTableRows} | |||
| columns={pickTableColumns} | |||
| getRowHeight={() => 'auto'} | |||
| getRowHeight={() => "auto"} | |||
| /> | |||
| </Box> | |||
| ); | |||
| @@ -381,7 +413,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Job Order Info")} /> | |||
| <Tab label={t("Job Order Lines")} /> | |||
| <Tab label={t("BoM Material")} /> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Production Process Line Remark")} /> | |||
| <Tab label={t("Matching Stock")} /> | |||
| @@ -34,7 +34,7 @@ interface ProductProcessListProps { | |||
| const PER_PAGE = 6; | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess, printerCombo }) => { | |||
| const { t } = useTranslation(); | |||
| const { t } = useTranslation( ["common", "production","purchaseOrder"]); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const sessionToken = session as SessionWithTokens | null; | |||
| const [loading, setLoading] = useState(false); | |||
| @@ -144,16 +144,22 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center"> | |||
| <Box sx={{ minWidth: 0 }}> | |||
| <Typography variant="subtitle1"> | |||
| {process.productProcessCode} | |||
| {t("Job Order")}: {jobOrderCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Job Order")}: {jobOrderCode} | |||
| </Typography> | |||
| </Box> | |||
| <Chip size="small" label={t(status)} color={statusColor as any} /> | |||
| </Stack> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Item Name")}: {process.itemName} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Required Qty")}: {process.requiredQty} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} | |||
| </Typography> | |||
| {statusLower !== "pending" && linesWithStatus.length > 0 && ( | |||
| <Box sx={{ mt: 1 }}> | |||
| <Typography variant="body2" fontWeight={600}> | |||
| @@ -184,9 +190,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| </Button> | |||
| )} | |||
| <Box sx={{ flex: 1 }} /> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Lines")}: {totalCount} | |||
| </Typography> | |||
| </CardActions> | |||
| </Card> | |||
| </Grid> | |||
| @@ -63,7 +63,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const [showOutputTable, setShowOutputTable] = useState(false); | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | |||
| // 检查是否两个都已扫描 | |||
| //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | |||
| @@ -98,6 +98,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| setLineDetail(null); | |||
| }); | |||
| }, [lineId]); | |||
| useEffect(() => { | |||
| if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) { | |||
| setRemainingTime(null); | |||
| return; | |||
| } | |||
| const start = new Date(lineDetail.startTime as any); | |||
| const end = new Date(start.getTime() + lineDetail.durationInMinutes * 60_000); | |||
| const update = () => { | |||
| const diff = end.getTime() - Date.now(); | |||
| if (diff <= 0) { | |||
| setRemainingTime("00:00"); | |||
| 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}`); | |||
| }; | |||
| update(); | |||
| const timer = setInterval(update, 1000); | |||
| return () => clearInterval(timer); | |||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); | |||
| const handleSubmitOutput = async () => { | |||
| if (!lineDetail?.id) return; | |||
| @@ -255,7 +276,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| </TableRow> | |||
| {/* By-product */} | |||
| {lineDetail?.byproductQty && lineDetail.byproductQty > 0 && ( | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| @@ -272,10 +293,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography>{lineDetail.byproductUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| {/* Defect */} | |||
| {lineDetail?.defectQty && lineDetail.defectQty > 0 && ( | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | |||
| @@ -287,10 +307,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography>{lineDetail.defectUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| {/* Scrap */} | |||
| {lineDetail?.scrapQty && lineDetail.scrapQty > 0 && ( | |||
| <TableRow sx={{ bgcolor: 'error.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | |||
| @@ -302,7 +320,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography>{lineDetail.scrapUom || "-"}</Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </CardContent> | |||
| @@ -311,8 +328,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <> | |||
| {/* 如果未完成,显示原来的两个部分 */} | |||
| {/* 当前步骤信息 */} | |||
| {!showOutputTable && ( | |||
| <Grid container spacing={2} sx={{ mb: 3 }}> | |||
| <Grid item xs={12} md={6}> | |||
| <Grid item xs={12} > | |||
| <Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||
| @@ -327,6 +345,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {equipmentName} | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | |||
| <Button | |||
| variant="contained" | |||
| @@ -355,27 +374,24 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| {t("Continue")} | |||
| </Button> | |||
| )} | |||
| <Button | |||
| sx={{ mt: 2, alignSelf: "flex-end" }} | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(true)} | |||
| > | |||
| {t("Order Complete")} | |||
| </Button> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </Grid> | |||
| </Grid> | |||
| )} | |||
| {/* ========== 产出输入表单 ========== */} | |||
| {showOutputTable && ( | |||
| <Box> | |||
| <Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |||
| <Typography variant="h6" fontWeight={600}> | |||
| {t("Production Output Data Entry")} | |||
| </Typography> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setShowOutputTable(!showOutputTable)} | |||
| > | |||
| {showOutputTable ? t("Hide Table") : t("Show Table")} | |||
| </Button> | |||
| </Box> | |||
| {showOutputTable && ( | |||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| @@ -526,8 +542,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| )} | |||
| </> | |||
| )} | |||
| </Box> | |||
| @@ -16,7 +16,6 @@ | |||
| "Add Record": "新增", | |||
| "Clean Record": "重置", | |||
| "Dashboard": "資訊展示面板", | |||
| "Store Management": "原材料", | |||
| "Stock Take Management": "盤點管理", | |||
| "Store Management": "倉庫管理", | |||
| "Delivery": "送貨訂單", | |||
| @@ -27,7 +26,7 @@ | |||
| "User Group": "用戶群組", | |||
| "Items": "物料", | |||
| "Demand Forecast Setting": "需求預測設定", | |||
| "Equipment Type": "設備類型", | |||
| "Equipment Type/Code": "使用設備-編號", | |||
| "Equipment": "設備", | |||
| "Warehouse": "倉庫", | |||
| "Supplier": "供應商", | |||
| @@ -107,6 +106,93 @@ | |||
| "Row per page": "每頁行數", | |||
| "No data available": "沒有資料", | |||
| "jodetail": "工單細節", | |||
| "Sign out": "登出" | |||
| "Sign out": "登出", | |||
| "By-product": "副產品", | |||
| "Complete Step": "完成步驟", | |||
| "Defect": "缺陷", | |||
| "Output from Process": "流程輸出", | |||
| "Quantity": "數量", | |||
| "Scrap": "廢料", | |||
| "Unit": "單位", | |||
| "Back to List": "返回列表", | |||
| "Production Output Data Entry": "生產輸出數據輸入", | |||
| "Step": "步驟", | |||
| "Quality Check": "品質檢查", | |||
| "Action": "操作", | |||
| "Changeover Time (mins)": "生產後轉換時間(分鐘)", | |||
| "Completed": "完成", | |||
| "completed": "完成", | |||
| "Date": "日期", | |||
| "Failed to submit scan data. Please try again.": "掃碼數據提交失敗. 請重試.", | |||
| "In Progress": "進行中", | |||
| "Is Dark": "是否黑暗", | |||
| "Is Dense": "是否密集", | |||
| "Is Float": "是否浮動", | |||
| "Job Order Code": "工單編號", | |||
| "Operator": "操作員", | |||
| "Output Qty": "輸出數量", | |||
| "Pending": "待處理", | |||
| "pending": "待處理", | |||
| "Please scan equipment code (optional if not required)": "請掃描設備編號(可選)", | |||
| "Please scan operator code": "請掃描操作員編號", | |||
| "Please scan operator code first": "請先掃描操作員編號", | |||
| "Processing Time (mins)": "步驟時間(分鐘)", | |||
| "Production Process Information": "生產流程信息", | |||
| "Production Process Steps": "生產流程步驟", | |||
| "Scan Operator & Equipment": "掃描操作員和設備", | |||
| "Seq": "序號", | |||
| "Setup Time (mins)": "生產前預備時間(分鐘)", | |||
| "Start": "開始", | |||
| "Start QR Scan": "開始掃碼", | |||
| "Status": "狀態", | |||
| "in_progress": "進行中", | |||
| "In_Progress": "進行中", | |||
| "inProgress": "進行中", | |||
| "Step Name": "步驟名稱", | |||
| "Stop QR Scan": "停止掃碼", | |||
| "Submit & Start": "提交並開始", | |||
| "Total Steps": "總步驟數", | |||
| "Unknown": "", | |||
| "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | |||
| "View": "查看", | |||
| "Back": "返回", | |||
| "BoM Material": "物料清單", | |||
| "Is Dark | Dense | Float": "是否黑暗 | 密集 | 浮動", | |||
| "Item Code": "物料編號", | |||
| "Item Name": "物料名稱", | |||
| "Job Order Info": "工單信息", | |||
| "Matching Stock": "匹配庫存", | |||
| "No data found": "沒有找到資料", | |||
| "Production Priority": "生產優先級", | |||
| "Production Process": "工藝流程", | |||
| "Production Process Line Remark": "工藝明細", | |||
| "Remark": "明細", | |||
| "Req. Qty": "需求數量", | |||
| "Seq No": "序號", | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Stock Status": "庫存狀態", | |||
| "Target Production Date": "目標生產日期", | |||
| "id": "ID", | |||
| "Finished lines": "完成行", | |||
| "Invalid Stock In Line Id": "無效庫存行ID", | |||
| "Production date": "生產日期", | |||
| "Required Qty": "需求數量", | |||
| "Total processes": "總流程數", | |||
| "View Details": "查看詳情", | |||
| "view stockin": "查看入庫", | |||
| "Completed Step": "完成步驟", | |||
| "Continue": "繼續", | |||
| "Executing": "執行中", | |||
| "Order Complete": "訂單完成", | |||
| "Pause": "暫停", | |||
| "Production Output Data": "生產輸出數據", | |||
| "Step Information": "步驟信息", | |||
| "Stop": "停止", | |||
| "Putaway Detail": "上架詳情" | |||
| } | |||