| @@ -1218,7 +1218,6 @@ export interface ProcessStatusInfo { | |||
| isRequired: boolean; | |||
| } | |||
| export interface JobProcessStatusResponse { | |||
| jobOrderId: number; | |||
| jobOrderCode: string; | |||
| @@ -1244,6 +1243,85 @@ export const fetchJobProcessStatus = cache(async (date?: string) => { | |||
| next: { tags: ["jobProcessStatus"] }, | |||
| }); | |||
| }); | |||
| // ===== Operator KPI Dashboard ===== | |||
| export interface OperatorKpiProcessInfo { | |||
| jobOrderId?: number | null; | |||
| jobOrderCode?: string | null; | |||
| productProcessId?: number | null; | |||
| productProcessLineId?: number | null; | |||
| processName?: string | null; | |||
| equipmentName?: string | null; | |||
| equipmentDetailName?: string | null; | |||
| startTime?: string | number[] | null; | |||
| endTime?: string | number[] | null; | |||
| processingTime?: number | null; | |||
| itemCode?: string | null; | |||
| itemName?: string | null; | |||
| } | |||
| export interface OperatorKpiResponse { | |||
| operatorId: number; | |||
| operatorName?: string | null; | |||
| staffNo?: string | null; | |||
| totalProcessingMinutes: number; | |||
| totalJobOrderCount: number; | |||
| currentProcesses: OperatorKpiProcessInfo[]; | |||
| } | |||
| export const fetchOperatorKpi = cache(async (date?: string) => { | |||
| const params = new URLSearchParams(); | |||
| if (date) params.set("date", date); | |||
| const qs = params.toString(); | |||
| const url = `${BASE_API_URL}/product-process/Demo/OperatorKpi${qs ? `?${qs}` : ""}`; | |||
| return serverFetchJson<OperatorKpiResponse[]>(url, { | |||
| method: "GET", | |||
| next: { tags: ["operatorKpi"] }, | |||
| }); | |||
| }); | |||
| // ===== Equipment Status Dashboard ===== | |||
| export interface EquipmentStatusProcessInfo { | |||
| jobOrderId?: number | null; | |||
| jobOrderCode?: string | null; | |||
| productProcessId?: number | null; | |||
| productProcessLineId?: number | null; | |||
| processName?: string | null; | |||
| operatorName?: string | null; | |||
| startTime?: string | number[] | null; | |||
| processingTime?: number | null; | |||
| } | |||
| export interface EquipmentStatusPerDetail { | |||
| equipmentDetailId: number; | |||
| equipmentDetailCode?: string | null; | |||
| equipmentDetailName?: string | null; | |||
| equipmentId?: number | null; | |||
| equipmentTypeName?: string | null; | |||
| status: string; | |||
| repairAndMaintenanceStatus?: boolean | null; | |||
| latestRepairAndMaintenanceDate?: string | null; | |||
| lastRepairAndMaintenanceDate?: string | null; | |||
| repairAndMaintenanceRemarks?: string | null; | |||
| currentProcess?: EquipmentStatusProcessInfo | null; | |||
| } | |||
| export interface EquipmentStatusByTypeResponse { | |||
| equipmentTypeId: number; | |||
| equipmentTypeName?: string | null; | |||
| details: EquipmentStatusPerDetail[]; | |||
| } | |||
| export const fetchEquipmentStatus = cache(async () => { | |||
| const url = `${BASE_API_URL}/product-process/Demo/EquipmentStatus`; | |||
| return serverFetchJson<EquipmentStatusByTypeResponse[]>(url, { | |||
| method: "GET", | |||
| next: { tags: ["equipmentStatus"] }, | |||
| }); | |||
| }); | |||
| export const deleteProductProcessLine = async (lineId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/delete/${lineId}`, | |||
| @@ -243,10 +243,12 @@ useEffect(() => { | |||
| return; | |||
| } | |||
| // ✅ 只允许 Verified>0 且没有问题时,走 normal pick | |||
| // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程 | |||
| const badPackageQty = Number((formData as any).badPackageQty) || 0; | |||
| const isNormalPick = verifiedQty > 0 | |||
| && formData.missQty == 0 | |||
| && formData.badItemQty == 0; | |||
| && formData.missQty == 0 | |||
| && formData.badItemQty == 0 | |||
| && badPackageQty == 0; | |||
| if (isNormalPick) { | |||
| if (onNormalPickSubmit) { | |||
| @@ -0,0 +1,363 @@ | |||
| "use client"; | |||
| import React, { useState, useEffect, useCallback } from "react"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| CircularProgress, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| Typography, | |||
| Tabs, | |||
| Tab, | |||
| Chip, | |||
| } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import dayjs from "dayjs"; | |||
| import { | |||
| fetchEquipmentStatus, | |||
| EquipmentStatusByTypeResponse, | |||
| EquipmentStatusPerDetail, | |||
| } from "@/app/api/jo/actions"; | |||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| const REFRESH_INTERVAL = 60 * 1000; // 1 分鐘 | |||
| const STATUS_COLORS: Record<string, "success" | "default" | "warning" | "error"> = { | |||
| Processing: "success", | |||
| Idle: "default", | |||
| Repair: "warning", | |||
| }; | |||
| const formatDateTime = (value: any): string => { | |||
| if (!value) return "-"; | |||
| if (Array.isArray(value)) { | |||
| try { | |||
| const parsed = arrayToDayjs(value, true); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("YYYY-MM-DD HH:mm"); | |||
| } | |||
| } catch (e) { | |||
| console.error("Error parsing datetime array:", e); | |||
| } | |||
| } | |||
| if (typeof value === "string") { | |||
| const parsed = dayjs(value); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("YYYY-MM-DD HH:mm"); | |||
| } | |||
| } | |||
| return "-"; | |||
| }; | |||
| const formatTime = (value: any): string => { | |||
| if (!value) return "-"; | |||
| if (Array.isArray(value)) { | |||
| try { | |||
| const parsed = arrayToDayjs(value, true); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("HH:mm"); | |||
| } | |||
| } catch (e) { | |||
| console.error("Error parsing time array:", e); | |||
| } | |||
| } | |||
| if (typeof value === "string") { | |||
| const parsed = dayjs(value); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("HH:mm"); | |||
| } | |||
| } | |||
| return "-"; | |||
| }; | |||
| // 计算预计完成时间 | |||
| const calculateEstimatedCompletionTime = ( | |||
| startTime: any, | |||
| processingTime: number | null | undefined | |||
| ): string => { | |||
| if (!startTime || !processingTime || processingTime <= 0) return "-"; | |||
| try { | |||
| const start = arrayToDayjs(startTime, true); | |||
| if (!start.isValid()) return "-"; | |||
| const estimated = start.add(processingTime, "minute"); | |||
| return estimated.format("YYYY-MM-DD HH:mm"); | |||
| } catch (e) { | |||
| console.error("Error calculating estimated completion time:", e); | |||
| return "-"; | |||
| } | |||
| }; | |||
| // 计算剩余时间(分钟) | |||
| const calculateRemainingTime = ( | |||
| startTime: any, | |||
| processingTime: number | null | undefined | |||
| ): string => { | |||
| if (!startTime || !processingTime || processingTime <= 0) return "-"; | |||
| try { | |||
| const start = arrayToDayjs(startTime, true); | |||
| if (!start.isValid()) return "-"; | |||
| const now = dayjs(); | |||
| const estimated = start.add(processingTime, "minute"); | |||
| const remainingMinutes = estimated.diff(now, "minute"); | |||
| if (remainingMinutes < 0) { | |||
| return `-${Math.abs(remainingMinutes)}`; | |||
| } | |||
| return remainingMinutes.toString(); | |||
| } catch (e) { | |||
| console.error("Error calculating remaining time:", e); | |||
| return "-"; | |||
| } | |||
| }; | |||
| const EquipmentStatusDashboard: React.FC = () => { | |||
| const { t } = useTranslation(["common", "jo"]); | |||
| const [data, setData] = useState<EquipmentStatusByTypeResponse[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [tabIndex, setTabIndex] = useState<number>(0); | |||
| const loadData = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const result = await fetchEquipmentStatus(); | |||
| setData(result || []); | |||
| } catch (error) { | |||
| console.error("Error fetching equipment status:", error); | |||
| setData([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| loadData(); | |||
| const interval = setInterval(() => { | |||
| loadData(); | |||
| }, REFRESH_INTERVAL); | |||
| return () => clearInterval(interval); | |||
| }, [loadData]); | |||
| // 添加定时更新剩余时间 | |||
| useEffect(() => { | |||
| const timer = setInterval(() => { | |||
| // 触发重新渲染以更新剩余时间 | |||
| setData((prev) => [...prev]); | |||
| }, 60000); // 每分钟更新一次 | |||
| return () => clearInterval(timer); | |||
| }, []); | |||
| const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { | |||
| setTabIndex(newValue); | |||
| }; | |||
| const displayTypes = | |||
| tabIndex === 0 | |||
| ? data | |||
| : data.filter((_, index) => index === tabIndex - 1); | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | |||
| <Typography variant="h5" sx={{ fontWeight: 600 }}> | |||
| {t("Production Equipment Status Dashboard")} | |||
| </Typography> | |||
| </Box> | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| sx={{ mb: 2 }} | |||
| variant="scrollable" | |||
| scrollButtons="auto" | |||
| > | |||
| <Tab label={t("All")} /> | |||
| {data.map((type, index) => ( | |||
| <Tab | |||
| key={type.equipmentTypeId} | |||
| label={type.equipmentTypeName || `${t("Equipment Type")} ${index + 1}`} | |||
| /> | |||
| ))} | |||
| </Tabs> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : displayTypes.length === 0 ? ( | |||
| <Box sx={{ textAlign: "center", p: 3 }}> | |||
| {t("No data available")} | |||
| </Box> | |||
| ) : ( | |||
| <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}> | |||
| {displayTypes.map((type) => { | |||
| const details = type.details || []; | |||
| if (details.length === 0) return null; | |||
| return ( | |||
| <Card | |||
| key={type.equipmentTypeId} | |||
| sx={{ | |||
| border: "3px solid #135fed", | |||
| overflowX: "auto", | |||
| }} | |||
| > | |||
| <CardContent> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> | |||
| {type.equipmentTypeName || "-"} | |||
| </Typography> | |||
| <TableContainer component={Paper}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Equipment Name and Code")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <Box sx={{ display: "flex", flexDirection: "column" }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {d.equipmentDetailName || "-"} | |||
| </Typography> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {d.equipmentDetailCode || "-"} | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {/* 工序 Row */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Process")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| {d.status === "Processing" ? d.currentProcess?.processName || "-" : "-"} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| {/* 狀態 Row - 修改:Processing 时只显示 job order code */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Status")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => { | |||
| const chipColor = STATUS_COLORS[d.status] || "default"; | |||
| const cp = d.currentProcess; | |||
| // Processing 时只显示 job order code,不显示 Chip | |||
| if (d.status === "Processing" && cp?.jobOrderCode) { | |||
| return ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <Typography variant="body2" sx={{ fontWeight: 500 }}> | |||
| {cp.jobOrderCode} | |||
| </Typography> | |||
| </TableCell> | |||
| ); | |||
| } | |||
| // 其他状态显示 Chip | |||
| return ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| <Chip label={t(`${d.status}`)} color={chipColor} size="small" /> | |||
| </TableCell> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| {/* 開始時間 Row */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Start Time")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| {d.status === "Processing" | |||
| ? formatDateTime(d.currentProcess?.startTime) | |||
| : "-"} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| {/* 預計完成時間 Row */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("預計完成時間")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| {d.status === "Processing" | |||
| ? calculateEstimatedCompletionTime( | |||
| d.currentProcess?.startTime, | |||
| d.currentProcess?.processingTime | |||
| ) | |||
| : "-"} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| {/* 剩餘時間 Row */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Remaining Time (min)")} | |||
| </Typography> | |||
| </TableCell> | |||
| {details.map((d) => ( | |||
| <TableCell key={d.equipmentDetailId} align="center"> | |||
| {d.status === "Processing" | |||
| ? calculateRemainingTime( | |||
| d.currentProcess?.startTime, | |||
| d.currentProcess?.processingTime | |||
| ) | |||
| : "-"} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| })} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default EquipmentStatusDashboard; | |||
| @@ -173,7 +173,7 @@ const JobProcessStatus: React.FC = () => { | |||
| <CardContent> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> | |||
| <Typography variant="h5" sx={{ fontWeight: 600 }}> | |||
| {t("Job Process Status")} | |||
| {t("Job Process Status Dashboard")} | |||
| </Typography> | |||
| <FormControl size="small" sx={{ minWidth: 160 }}> | |||
| @@ -194,21 +194,27 @@ const JobProcessStatus: React.FC = () => { | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer component={Paper}> | |||
| <Table size="small" sx={{ minWidth: 1200 }}> | |||
| <TableContainer | |||
| component={Paper} | |||
| sx={{ | |||
| border: '3px solid #135fed', | |||
| overflowX: 'auto', // 关键:允许横向滚动 | |||
| }} | |||
| > | |||
| <Table size="small" sx={{ minWidth: 1800 }}> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell rowSpan={3}> | |||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Job Order No.")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell rowSpan={3}> | |||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("FG / WIP Item")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell rowSpan={3}> | |||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Production Time Remaining")} | |||
| </Typography> | |||
| @@ -216,8 +222,8 @@ const JobProcessStatus: React.FC = () => { | |||
| </TableRow> | |||
| <TableRow> | |||
| {[1, 2, 3, 4, 5, 6].map((num) => ( | |||
| <TableCell key={num} align="center"> | |||
| {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( | |||
| <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||
| {t("Process")} {num} | |||
| </Typography> | |||
| @@ -225,9 +231,9 @@ const JobProcessStatus: React.FC = () => { | |||
| ))} | |||
| </TableRow> | |||
| <TableRow> | |||
| {[1, 2, 3, 4, 5, 6].map((num) => ( | |||
| <TableCell key={num} align="center"> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| {Array.from({ length: 16 }, (_, i) => i + 1).map((num) => ( | |||
| <TableCell key={num} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | |||
| <Typography variant="caption" sx={{ fontWeight: 600 }}> | |||
| {t("Start")} | |||
| </Typography> | |||
| @@ -245,21 +251,21 @@ const JobProcessStatus: React.FC = () => { | |||
| <TableBody> | |||
| {data.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={9} align="center"> | |||
| <TableCell colSpan={9} align="center" sx={{ padding: '20px' }}> | |||
| {t("No data available")} | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| data.map((row) => ( | |||
| <TableRow key={row.jobOrderId}> | |||
| <TableCell> | |||
| <TableCell sx={{ padding: '16px 20px' }}> | |||
| {row.jobOrderCode || '-'} | |||
| </TableCell> | |||
| <TableCell> | |||
| <Box>{row.itemCode || '-'}</Box> | |||
| <TableCell sx={{ padding: '16px 20px' }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>{row.itemCode || '-'}</Box> | |||
| <Box>{row.itemName || '-'}</Box> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell sx={{ padding: '16px 20px' }}> | |||
| {row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)} | |||
| </TableCell> | |||
| @@ -276,7 +282,7 @@ const JobProcessStatus: React.FC = () => { | |||
| // 如果工序不是必需的,只显示一个 N/A | |||
| if (!process.isRequired) { | |||
| return ( | |||
| <TableCell key={index} align="center"> | |||
| <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Typography variant="body2"> | |||
| N/A | |||
| </Typography> | |||
| @@ -290,17 +296,18 @@ const JobProcessStatus: React.FC = () => { | |||
| ].filter(Boolean).join(" "); | |||
| // 如果工序是必需的,显示三行(Start、Finish、Wait Time) | |||
| return ( | |||
| <TableCell key={index} align="center"> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||
| <Typography variant="body2">{label || "-"}</Typography> | |||
| <Typography variant="body2"> | |||
| <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> | |||
| <Typography variant="body2" sx={{ mb: 0.5 }}>{label || "-"}</Typography> | |||
| <Typography variant="body2" sx={{ py: 0.5 }}> | |||
| {formatTime(process.startTime)} | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| <Typography variant="body2" sx={{ py: 0.5 }}> | |||
| {formatTime(process.endTime)} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ | |||
| color: waitTime !== '-' && parseInt(waitTime) > 0 ? 'warning.main' : 'text.primary' | |||
| color: waitTime !== '-' && parseInt(waitTime) > 0 ? 'warning.main' : 'text.primary', | |||
| py: 0.5 | |||
| }}> | |||
| {waitTime} | |||
| </Typography> | |||
| @@ -0,0 +1,343 @@ | |||
| "use client"; | |||
| import React, { useState, useEffect, useCallback, useRef } from "react"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| CircularProgress, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| Typography, | |||
| FormControl, | |||
| Select, | |||
| MenuItem, | |||
| } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import dayjs from "dayjs"; | |||
| import { fetchOperatorKpi, OperatorKpiResponse, OperatorKpiProcessInfo } from "@/app/api/jo/actions"; | |||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 分鐘 | |||
| const OperatorKpiDashboard: React.FC = () => { | |||
| const { t } = useTranslation(["common", "jo"]); | |||
| const [data, setData] = useState<OperatorKpiResponse[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD")); | |||
| const refreshCountRef = useRef<number>(0); | |||
| const formatTime = (timeData: any): string => { | |||
| if (!timeData) return "-"; | |||
| if (Array.isArray(timeData)) { | |||
| try { | |||
| const parsed = arrayToDayjs(timeData, true); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("HH:mm"); | |||
| } | |||
| } catch (e) { | |||
| console.error("Error parsing time array:", e); | |||
| } | |||
| } | |||
| if (typeof timeData === "string") { | |||
| const parsed = dayjs(timeData); | |||
| if (parsed.isValid()) { | |||
| return parsed.format("HH:mm"); | |||
| } | |||
| } | |||
| return "-"; | |||
| }; | |||
| const formatMinutesToHHmm = (minutes: number): string => { | |||
| if (!minutes || minutes <= 0) return "00:00"; | |||
| const hours = Math.floor(minutes / 60); | |||
| const mins = minutes % 60; | |||
| return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`; | |||
| }; | |||
| const loadData = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const result = await fetchOperatorKpi(selectedDate); | |||
| setData(result); | |||
| refreshCountRef.current += 1; | |||
| } catch (error) { | |||
| console.error("Error fetching operator KPI:", error); | |||
| setData([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [selectedDate]); | |||
| useEffect(() => { | |||
| loadData(); | |||
| const interval = setInterval(() => { | |||
| loadData(); | |||
| }, REFRESH_INTERVAL); | |||
| return () => clearInterval(interval); | |||
| }, [loadData]); | |||
| const renderCurrentProcesses = (processes: OperatorKpiProcessInfo[]) => { | |||
| if (!processes || processes.length === 0) { | |||
| return ( | |||
| <Typography variant="body2" color="text.secondary" sx={{ py: 1 }}> | |||
| - | |||
| </Typography> | |||
| ); | |||
| } | |||
| // 只顯示目前一個處理中的工序(樣式比照 Excel:欄位名稱縱向排列) | |||
| const p = processes[0]; | |||
| const jobOrder = p.jobOrderCode ? `[${p.jobOrderCode}]` : "-"; | |||
| const itemInfo = p.itemCode && p.itemName | |||
| ? `${p.itemCode} - ${p.itemName}` | |||
| : p.itemCode || p.itemName || "-"; | |||
| // 格式化所需時間(分鐘轉換為 HH:mm) | |||
| const formatRequiredTime = (minutes: number | null | undefined): string => { | |||
| if (!minutes || minutes <= 0) return "-"; | |||
| const hours = Math.floor(minutes / 60); | |||
| const mins = minutes % 60; | |||
| return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`; | |||
| }; | |||
| // 計算預計完成時間 | |||
| const calculateEstimatedCompletionTime = (): string => { | |||
| if (!p.startTime || !p.processingTime || p.processingTime <= 0) return "-"; | |||
| try { | |||
| const start = arrayToDayjs(p.startTime, true); | |||
| if (!start.isValid()) return "-"; | |||
| const estimated = start.add(p.processingTime, "minute"); | |||
| return estimated.format("HH:mm"); | |||
| } catch (e) { | |||
| console.error("Error calculating estimated completion time:", e); | |||
| return "-"; | |||
| } | |||
| }; | |||
| return ( | |||
| <> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Job Order and Product")}: {jobOrder} {itemInfo} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Process")}: {p.processName || "-"} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Start Time")}: {formatTime(p.startTime)} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Required Time")}: {formatRequiredTime(p.processingTime)} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Estimated Completion Time")}: {calculateEstimatedCompletionTime()} | |||
| </Typography> | |||
| </> | |||
| ); | |||
| }; | |||
| return ( | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 3 }}> | |||
| <Typography variant="h5" sx={{ fontWeight: 600 }}> | |||
| {t("Operator KPI Dashboard")} | |||
| </Typography> | |||
| <FormControl size="small" sx={{ minWidth: 160 }}> | |||
| <Select | |||
| value={selectedDate} | |||
| onChange={(e) => setSelectedDate(e.target.value)} | |||
| > | |||
| <MenuItem value={dayjs().format("YYYY-MM-DD")}>{t("Today")}</MenuItem> | |||
| <MenuItem value={dayjs().subtract(1, "day").format("YYYY-MM-DD")}>{t("Yesterday")}</MenuItem> | |||
| <MenuItem value={dayjs().subtract(2, "day").format("YYYY-MM-DD")}>{t("Two Days Ago")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <TableContainer | |||
| component={Paper} | |||
| sx={{ | |||
| border: "3px solid #135fed", | |||
| overflowX: "auto", | |||
| }} | |||
| > | |||
| <Table size="small" sx={{ minWidth: 800 }}> | |||
| <TableHead> | |||
| <TableRow | |||
| sx={{ | |||
| bgcolor: "#424242", | |||
| "& th": { | |||
| borderBottom: "none", | |||
| py: 1.5, | |||
| }, | |||
| }} | |||
| > | |||
| <TableCell sx={{ width: 80 }}> | |||
| <Typography | |||
| variant="subtitle2" | |||
| sx={{ | |||
| fontWeight: 600, | |||
| //color: "#ffffff", | |||
| }} | |||
| > | |||
| {t("No.")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell sx={{ minWidth: 280 }}> | |||
| <Typography | |||
| variant="subtitle2" | |||
| sx={{ | |||
| fontWeight: 600, | |||
| //color: "#ffffff", | |||
| }} | |||
| > | |||
| {t("Operator")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| <Typography | |||
| variant="subtitle2" | |||
| sx={{ | |||
| fontWeight: 600, | |||
| //color: "#ffffff", | |||
| }} | |||
| > | |||
| {t("Job Details")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {data.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={4} align="center"> | |||
| {t("No data available")} | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| data.map((row, index) => { | |||
| const jobOrderCount = row.totalJobOrderCount || 0; | |||
| return ( | |||
| <TableRow | |||
| key={row.operatorId} | |||
| sx={{ | |||
| "&:hover": { | |||
| bgcolor: "#f9f9f9", | |||
| }, | |||
| "& td": { | |||
| borderBottom: "1px solid #e0e0e0", | |||
| py: 2, | |||
| verticalAlign: "top", | |||
| }, | |||
| }} | |||
| > | |||
| <TableCell | |||
| sx={{ | |||
| width: 80, | |||
| textAlign: "center", | |||
| fontWeight: 500, | |||
| verticalAlign: "top", | |||
| }} | |||
| > | |||
| {index + 1} | |||
| </TableCell> | |||
| <TableCell | |||
| sx={{ | |||
| minWidth: 280, | |||
| padding: 0, | |||
| verticalAlign: "top", | |||
| height: "100%", | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| p: 1.5, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 0.75, | |||
| bgcolor: "#f5f5f5", | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 1.5, | |||
| boxSizing: "border-box", | |||
| height: "180px", | |||
| }} | |||
| > | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Operator Name & No.")}:{" "} | |||
| <Box component="span" sx={{ fontWeight: 500 }}> | |||
| {row.operatorName || "-"}{" "} | |||
| {row.staffNo ? `(${row.staffNo})` : ""} | |||
| </Box> | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Count of Job Orders")}:{" "} | |||
| <Box component="span" sx={{ fontWeight: 500 }}> | |||
| {jobOrderCount} | |||
| </Box> | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ lineHeight: 1.6 }}> | |||
| {t("Total Processing Time")}:{" "} | |||
| <Box component="span" sx={{ fontWeight: 500 }}> | |||
| {formatMinutesToHHmm(row.totalProcessingMinutes || 0)} | |||
| </Box> | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell | |||
| sx={{ | |||
| minWidth: 300, | |||
| padding: 0, | |||
| verticalAlign: "top", | |||
| height: "100%", | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| p: 1.5, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| gap: 0.75, | |||
| bgcolor: "#f5f5f5", | |||
| border: "1px solid #e0e0e0", | |||
| borderRadius: 1.5, | |||
| boxSizing: "border-box", | |||
| height: "180px", | |||
| }} | |||
| > | |||
| {renderCurrentProcesses(row.currentProcesses)} | |||
| </Box> | |||
| </TableCell> | |||
| </TableRow> | |||
| ); | |||
| }) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| )} | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default OperatorKpiDashboard; | |||
| @@ -246,7 +246,13 @@ const isStockSufficient = (line: JobOrderLineInfo) => { | |||
| const stockCounts = useMemo(() => { | |||
| // 过滤掉 consumables 类型的 lines | |||
| const nonConsumablesLines = jobOrderLines.filter( | |||
| line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm" | |||
| line => { | |||
| const type = line.type?.toLowerCase(); | |||
| return type !== "consumables" && | |||
| type !== "consumable" && // ✅ 添加单数形式 | |||
| type !== "cmb" && | |||
| type !== "nm" | |||
| } | |||
| ); | |||
| const total = nonConsumablesLines.length; | |||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | |||
| @@ -473,7 +479,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| field: "itemCode", | |||
| headerName: t("Material Code"), | |||
| flex: 0.6, | |||
| sortable: false, // ✅ 禁用排序 | |||
| sortable: false, | |||
| }, | |||
| { | |||
| field: "itemName", | |||
| @@ -490,11 +496,11 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| flex: 0.7, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| sortable: false, // ✅ 禁用排序 | |||
| // ✅ 将切换功能移到 header | |||
| sortable: false, | |||
| renderHeader: () => { | |||
| const qty = showBaseQty ? t("Base") : t("Req"); | |||
| const uom = showBaseQty ? t("Base UOM") : t(" "); | |||
| const uom = showBaseQty ? t("Base UOM") : t("Bom Uom"); | |||
| return ( | |||
| <Box | |||
| onClick={toggleBaseQty} | |||
| @@ -508,7 +514,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| }, | |||
| }} | |||
| > | |||
| {t("Bom Req. Qty")} ({uom}) | |||
| <Typography variant="body2"> | |||
| {t("Bom Req. Qty")}<br/> | |||
| ({uom}) | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| @@ -547,7 +556,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| }, | |||
| }} | |||
| > | |||
| {t("Stock Req. Qty")} ({uom}) | |||
| <Typography variant="body2"> | |||
| {t("Stock Req. Qty")} <br/> | |||
| ({uom}) | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| @@ -587,7 +599,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| }, | |||
| }} | |||
| > | |||
| {t("Stock Available")} ({uom}) | |||
| <Typography variant="body2"> | |||
| {t("Stock Available")} <br/> | |||
| ({uom}) | |||
| </Typography> | |||
| </Box> | |||
| ); | |||
| }, | |||
| @@ -684,7 +699,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| </Card> | |||
| <StyledDataGrid | |||
| sx={{ "--DataGrid-overlayHeight": "100px" }} | |||
| sx={{ "--DataGrid-overlayHeight": "200px" }} | |||
| disableColumnMenu | |||
| rows={pickTableRows} | |||
| columns={pickTableColumns} | |||
| @@ -9,6 +9,8 @@ import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/Prod | |||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | |||
| import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | |||
| import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; | |||
| import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard"; | |||
| import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard"; | |||
| import { | |||
| fetchProductProcesses, | |||
| fetchProductProcessesByJobOrderId, | |||
| @@ -165,7 +167,9 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | |||
| <Tab label={t("Production Process")} /> | |||
| <Tab label={t("Finished QC Job Orders")} /> | |||
| <Tab label={t("Job Process Status")} /> | |||
| <Tab label={t("Job Process Status Dashboard")} /> | |||
| <Tab label={t("Operator KPI Dashboard")} /> | |||
| <Tab label={t("Production Equipment Status Dashboard")} /> | |||
| </Tabs> | |||
| {tabIndex === 0 && ( | |||
| @@ -195,6 +199,12 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||
| {tabIndex === 2 && ( | |||
| <JobProcessStatus /> | |||
| )} | |||
| {tabIndex === 3 && ( | |||
| <OperatorKpiDashboard /> | |||
| )} | |||
| {tabIndex === 4 && ( | |||
| <EquipmentStatusDashboard /> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -76,7 +76,8 @@ const SubmitIssueForm: React.FC<Props> = ({ | |||
| }; | |||
| const handleSubmit = async () => { | |||
| if (!lotId || !submitQty || parseFloat(submitQty) <= 0) { | |||
| if (!lotId || !submitQty || parseFloat(submitQty) < 0) { | |||
| alert(t("Please enter a valid quantity")); | |||
| return; | |||
| } | |||
| @@ -175,7 +176,7 @@ const SubmitIssueForm: React.FC<Props> = ({ | |||
| <Button | |||
| onClick={handleSubmit} | |||
| variant="contained" | |||
| disabled={submitting || !submitQty || parseFloat(submitQty) <= 0} | |||
| disabled={submitting || !submitQty || parseFloat(submitQty) < 0} | |||
| > | |||
| {submitting ? t("Submitting...") : t("Submit")} | |||
| </Button> | |||
| @@ -15,7 +15,7 @@ | |||
| "Search": "搜索", | |||
| "This lot is rejected, please scan another lot.": "此批次已封存,請掃描另一個批號。", | |||
| "Process Start Time": "工序開始時間", | |||
| "Stock Req. Qty": "需求數(庫存單位)", | |||
| "Stock Req. Qty": "需求數", | |||
| "Staff No Required": "員工編號必填", | |||
| "User Not Found": "用戶不存在", | |||
| "Time Remaining": "剩餘時間", | |||
| @@ -52,7 +52,7 @@ | |||
| "No": "沒有", | |||
| "Assignment failed: ": "分配失敗: ", | |||
| "Unknown error": "未知錯誤", | |||
| "Job Process Status": "工單流程狀態", | |||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | |||
| "Total Time": "總時間", | |||
| "Remaining Time": "剩餘時間", | |||
| @@ -94,7 +94,7 @@ | |||
| "Deliver Order": "送貨訂單", | |||
| "Project": "專案", | |||
| "Product": "產品", | |||
| "Material": "材料", | |||
| "mat": "原料", | |||
| "consumables": "消耗品", | |||
| "non-consumables": "非消耗品", | |||
| @@ -109,7 +109,7 @@ | |||
| "Detail Scheduling": "詳細排程", | |||
| "Customer": "客戶", | |||
| "qcItem": "品檢項目", | |||
| "Item": "成品/半成品", | |||
| "Today": "今天", | |||
| "Yesterday": "昨天", | |||
| "Input Equipment is not match with process": "輸入的設備與流程不匹配", | |||
| @@ -209,9 +209,10 @@ | |||
| "Row per page": "每頁行數", | |||
| "Select Unit": "選擇單位", | |||
| "No data available": "沒有資料", | |||
| "Bom Req. Qty": "需求數(BOM單位)", | |||
| "Bom Req. Qty": "BOM", | |||
| "Material Name": "材料清單", | |||
| "Material Code": "材料清單", | |||
| "Bom UOM": "使用單位", | |||
| "Base UOM": "基本單位", | |||
| "Stock UOM": "庫存單位", | |||
| "jodetail": "工單細節", | |||
| @@ -238,7 +239,7 @@ | |||
| "Is Dense": "濃淡", | |||
| "Is Float": "浮沉", | |||
| "Job Order Code": "工單編號", | |||
| "Operator": "操作員", | |||
| "Output Qty": "輸出數量", | |||
| "Pending": "待處理", | |||
| "pending": "待處理", | |||
| @@ -264,10 +265,10 @@ | |||
| "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | |||
| "View": "查看", | |||
| "Back": "返回", | |||
| "BoM Material": "成品/半成品清單", | |||
| "BoM Material": "材料清單", | |||
| "N/A": "不適用", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度", | |||
| "Item Code": "成品/半成品名稱", | |||
| "Item Code": "材料名稱", | |||
| "Please scan equipment code": "請掃描設備編號", | |||
| "Equipment Code": "設備編號", | |||
| "Seq": "步驟", | |||
| @@ -283,7 +284,7 @@ | |||
| "Seq No": "加入步驟", | |||
| "Total pick orders": "總提料單數量", | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Stock Available": "庫存數", | |||
| "Confirm": "確認", | |||
| "Do you want to delete?": "您確定要刪除嗎?", | |||
| "Stock Status": "庫存狀態", | |||
| @@ -300,6 +301,7 @@ | |||
| "update production priority": "更新生產優先序", | |||
| "Assume Time Need": "預計所需時間", | |||
| "Required Qty": "需求數", | |||
| "Bom Required Qty": "Bom 使用份量", | |||
| "Total processes": "總流程數", | |||
| "View Details": "查看詳情", | |||
| "view stockin": "品檢", | |||
| @@ -412,6 +414,22 @@ | |||
| "Equipment Code": "設備編號", | |||
| "Yes": "是", | |||
| "No": "否", | |||
| "No.": "編號", | |||
| "Operator Name & No.": "操作員名稱及編號", | |||
| "Count of Job Orders": "已處理工單", | |||
| "Total Processing Time": "總工時", | |||
| "Material Pick Status": "物料提料狀態", | |||
| "Operator KPI Dashboard": "儀表板 - 操作員KPI概覽", | |||
| "Operator": "員工資訊", | |||
| "Equipment Name and Code": "設備名稱及編號", | |||
| "Remaining Time (min)": "剩餘時間(分鐘)", | |||
| "Production Equipment Status Dashboard": "儀表板 - 生產設備狀態", | |||
| "Idle": "閒置", | |||
| "Process": "工序", | |||
| "Job Details": "工單編號及生產產品", | |||
| "Required Time": "所需時間", | |||
| "Estimated Completion Time": "預計完成時間", | |||
| "Job Order and Product": "工單及貨品", | |||
| "Update Equipment Maintenance and Repair": "更新設備的維護和保養", | |||
| "Equipment Information": "設備資訊", | |||
| "Loading": "載入中...", | |||
| @@ -11,9 +11,12 @@ | |||
| "Status": "來貨狀態", | |||
| "Order Date From": "訂單日期", | |||
| "Delivery Order Code": "送貨訂單編號", | |||
| "Select Remark": "選擇備註", | |||
| "Confirm Assignment": "確認分配", | |||
| "Required Date": "所需日期", | |||
| "Submit Miss Item": "提交缺貨品", | |||
| "Submit Quantity": "提交數量", | |||
| "Store": "位置", | |||
| "Lane Code": "車線號碼", | |||
| "Available Orders": "可用訂單", | |||
| @@ -33,6 +33,11 @@ | |||
| "Start Time": "開始時間", | |||
| "Difference": "差異", | |||
| "stockTaking": "盤點中", | |||
| "Pick Order Code": "提料單編號", | |||
| "DO Order Code": "送貨單編號", | |||
| "JO Order Code": "工單編號", | |||
| "Picker Name": "提料員", | |||
| "rejected": "已拒絕", | |||
| "miss": "缺貨", | |||
| "bad": "不良", | |||
| @@ -11,11 +11,11 @@ | |||
| "Picked Qty": "已提料數量", | |||
| "Confirm All": "確認所有提料", | |||
| "Wait Time [minutes]": "等待時間(分鐘)", | |||
| "Job Process Status": "工單流程狀態", | |||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | |||
| "This lot is rejected, please scan another lot.": "此批次已拒收,請掃描另一個批次。", | |||
| "Edit": "改數", | |||
| "Just Complete": "已完成", | |||
| "Stock Req. Qty": "需求數(庫存單位)", | |||
| "Stock Req. Qty": "需求數", | |||
| "Bad Package Qty": "不良包裝數量", | |||
| "Progress": "進度", | |||
| "Search Job Order/ Create Job Order":"搜尋工單/建立工單", | |||
| @@ -100,7 +100,7 @@ | |||
| "Pause Reason": "暫停原因", | |||
| "Bag Usage": "包裝袋使用記錄", | |||
| "Reason": "原因", | |||
| "Stock Available": "倉庫可用數", | |||
| "update production priority": "更新生產優先序", | |||
| "Staff No": "員工編號", | |||
| "Please scan staff no": "請掃描員工編號", | |||
| @@ -108,7 +108,7 @@ | |||
| "Total lines: ": "所需貨品項目數量: ", | |||
| "Lines with sufficient stock: ": "可提料項目數量: ", | |||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | |||
| "Item Name": "成品/半成品", | |||
| "Item Name": "材料名稱", | |||
| "Material Code": "材料編號", | |||
| "Select Unit": "選擇單位", | |||
| "Job Order Pickexcution": "工單提料", | |||
| @@ -212,7 +212,8 @@ | |||
| "No Group": "沒有組", | |||
| "No created items": "沒有創建物料", | |||
| "Order Quantity": "需求數", | |||
| "Bom Req. Qty": "需求數(BOM單位)", | |||
| "Bom Req. Qty": "BOM", | |||
| "Bom Uom": "使用單位", | |||
| "Selected": "已選擇", | |||
| "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", | |||
| "Please select item": "請選擇物料", | |||
| @@ -421,7 +422,7 @@ | |||
| "View": "查看", | |||
| "Back": "返回", | |||
| "N/A": "不適用", | |||
| "BoM Material": "成品/半成品清單", | |||
| "BoM Material": "材料清單", | |||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | |||
| "Enter the number of cartons: ": "請輸入箱數:", | |||
| "Number of cartons": "箱數", | |||
| @@ -443,7 +444,7 @@ | |||
| "Req. Qty": "需求數量", | |||
| "Seq No": "加入步驟", | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Stock Available": "庫存數", | |||
| "Stock Status": "庫存狀態", | |||
| "Target Production Date": "目標生產日期", | |||
| "Description": "描述", | |||
| @@ -536,13 +537,22 @@ | |||
| "Finished Good Order": "成品出倉", | |||
| "finishedGood": "成品", | |||
| "Router": "執貨路線", | |||
| "Equipment Name and Code": "設備名稱及編號", | |||
| "Remaining Time (min)": "剩餘時間(分鐘)", | |||
| "Idle": "閒置", | |||
| "Repair": "維修", | |||
| "Production Equipment Status Dashboard": "儀表板 - 生產設備狀態", | |||
| "Operator KPI Dashboard": "儀表板 - 操作員KPI概覽", | |||
| "Start Scan": "開始掃碼", | |||
| "Stop Scan": "停止掃碼", | |||
| "Operator Name & No.": "操作員名稱及編號", | |||
| "Count of Job Orders": "工單數量", | |||
| "Total Processing Time": "總生產時間", | |||
| "Material Pick Status": "物料提料狀態", | |||
| "Job Order Qty": "工單數量", | |||
| "Sign out": "登出", | |||
| "Job Order No.": "工單編號", | |||
| "Operator KPI": "操作員KPI", | |||
| "FG / WIP Item": "成品/半成品", | |||
| "Production Time Remaining": "生產剩餘時間", | |||
| "Process": "工序", | |||