| @@ -1218,7 +1218,6 @@ export interface ProcessStatusInfo { | |||||
| isRequired: boolean; | isRequired: boolean; | ||||
| } | } | ||||
| export interface JobProcessStatusResponse { | export interface JobProcessStatusResponse { | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| jobOrderCode: string; | jobOrderCode: string; | ||||
| @@ -1244,6 +1243,85 @@ export const fetchJobProcessStatus = cache(async (date?: string) => { | |||||
| next: { tags: ["jobProcessStatus"] }, | 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) => { | export const deleteProductProcessLine = async (lineId: number) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/delete/${lineId}`, | `${BASE_API_URL}/product-process/Demo/ProcessLine/delete/${lineId}`, | ||||
| @@ -243,10 +243,12 @@ useEffect(() => { | |||||
| return; | return; | ||||
| } | } | ||||
| // ✅ 只允许 Verified>0 且没有问题时,走 normal pick | |||||
| // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程 | |||||
| const badPackageQty = Number((formData as any).badPackageQty) || 0; | |||||
| const isNormalPick = verifiedQty > 0 | const isNormalPick = verifiedQty > 0 | ||||
| && formData.missQty == 0 | |||||
| && formData.badItemQty == 0; | |||||
| && formData.missQty == 0 | |||||
| && formData.badItemQty == 0 | |||||
| && badPackageQty == 0; | |||||
| if (isNormalPick) { | if (isNormalPick) { | ||||
| if (onNormalPickSubmit) { | 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> | <CardContent> | ||||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> | <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> | ||||
| <Typography variant="h5" sx={{ fontWeight: 600 }}> | <Typography variant="h5" sx={{ fontWeight: 600 }}> | ||||
| {t("Job Process Status")} | |||||
| {t("Job Process Status Dashboard")} | |||||
| </Typography> | </Typography> | ||||
| <FormControl size="small" sx={{ minWidth: 160 }}> | <FormControl size="small" sx={{ minWidth: 160 }}> | ||||
| @@ -194,21 +194,27 @@ const JobProcessStatus: React.FC = () => { | |||||
| <CircularProgress /> | <CircularProgress /> | ||||
| </Box> | </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> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell rowSpan={3}> | |||||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | ||||
| {t("Job Order No.")} | {t("Job Order No.")} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell rowSpan={3}> | |||||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | ||||
| {t("FG / WIP Item")} | {t("FG / WIP Item")} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell rowSpan={3}> | |||||
| <TableCell rowSpan={3} sx={{ padding: '16px 20px' }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | ||||
| {t("Production Time Remaining")} | {t("Production Time Remaining")} | ||||
| </Typography> | </Typography> | ||||
| @@ -216,8 +222,8 @@ const JobProcessStatus: React.FC = () => { | |||||
| </TableRow> | </TableRow> | ||||
| <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 }}> | <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | ||||
| {t("Process")} {num} | {t("Process")} {num} | ||||
| </Typography> | </Typography> | ||||
| @@ -225,9 +231,9 @@ const JobProcessStatus: React.FC = () => { | |||||
| ))} | ))} | ||||
| </TableRow> | </TableRow> | ||||
| <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 }}> | <Typography variant="caption" sx={{ fontWeight: 600 }}> | ||||
| {t("Start")} | {t("Start")} | ||||
| </Typography> | </Typography> | ||||
| @@ -245,21 +251,21 @@ const JobProcessStatus: React.FC = () => { | |||||
| <TableBody> | <TableBody> | ||||
| {data.length === 0 ? ( | {data.length === 0 ? ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={9} align="center"> | |||||
| <TableCell colSpan={9} align="center" sx={{ padding: '20px' }}> | |||||
| {t("No data available")} | {t("No data available")} | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| data.map((row) => ( | data.map((row) => ( | ||||
| <TableRow key={row.jobOrderId}> | <TableRow key={row.jobOrderId}> | ||||
| <TableCell> | |||||
| <TableCell sx={{ padding: '16px 20px' }}> | |||||
| {row.jobOrderCode || '-'} | {row.jobOrderCode || '-'} | ||||
| </TableCell> | </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> | <Box>{row.itemName || '-'}</Box> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <TableCell sx={{ padding: '16px 20px' }}> | |||||
| {row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)} | {row.status === 'pending' ? '-' : calculateRemainingTime(row.planEndTime, row.processingTime, row.setupTime, row.changeoverTime)} | ||||
| </TableCell> | </TableCell> | ||||
| @@ -276,7 +282,7 @@ const JobProcessStatus: React.FC = () => { | |||||
| // 如果工序不是必需的,只显示一个 N/A | // 如果工序不是必需的,只显示一个 N/A | ||||
| if (!process.isRequired) { | if (!process.isRequired) { | ||||
| return ( | return ( | ||||
| <TableCell key={index} align="center"> | |||||
| <TableCell key={index} align="center" sx={{ padding: '16px 20px', minWidth: 150 }}> | |||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| N/A | N/A | ||||
| </Typography> | </Typography> | ||||
| @@ -290,17 +296,18 @@ const JobProcessStatus: React.FC = () => { | |||||
| ].filter(Boolean).join(" "); | ].filter(Boolean).join(" "); | ||||
| // 如果工序是必需的,显示三行(Start、Finish、Wait Time) | // 如果工序是必需的,显示三行(Start、Finish、Wait Time) | ||||
| return ( | 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)} | {formatTime(process.startTime)} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2"> | |||||
| <Typography variant="body2" sx={{ py: 0.5 }}> | |||||
| {formatTime(process.endTime)} | {formatTime(process.endTime)} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" sx={{ | <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} | {waitTime} | ||||
| </Typography> | </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(() => { | const stockCounts = useMemo(() => { | ||||
| // 过滤掉 consumables 类型的 lines | // 过滤掉 consumables 类型的 lines | ||||
| const nonConsumablesLines = jobOrderLines.filter( | 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 total = nonConsumablesLines.length; | ||||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | ||||
| @@ -473,7 +479,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| field: "itemCode", | field: "itemCode", | ||||
| headerName: t("Material Code"), | headerName: t("Material Code"), | ||||
| flex: 0.6, | flex: 0.6, | ||||
| sortable: false, // ✅ 禁用排序 | |||||
| sortable: false, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "itemName", | field: "itemName", | ||||
| @@ -490,11 +496,11 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| flex: 0.7, | flex: 0.7, | ||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| sortable: false, // ✅ 禁用排序 | |||||
| // ✅ 将切换功能移到 header | |||||
| sortable: false, | |||||
| renderHeader: () => { | 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 ( | return ( | ||||
| <Box | <Box | ||||
| onClick={toggleBaseQty} | 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> | </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> | </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> | </Box> | ||||
| ); | ); | ||||
| }, | }, | ||||
| @@ -684,7 +699,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| </Card> | </Card> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| sx={{ "--DataGrid-overlayHeight": "100px" }} | |||||
| sx={{ "--DataGrid-overlayHeight": "200px" }} | |||||
| disableColumnMenu | disableColumnMenu | ||||
| rows={pickTableRows} | rows={pickTableRows} | ||||
| columns={pickTableColumns} | columns={pickTableColumns} | ||||
| @@ -9,6 +9,8 @@ import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/Prod | |||||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | ||||
| import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; | ||||
| import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; | import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; | ||||
| import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard"; | |||||
| import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard"; | |||||
| import { | import { | ||||
| fetchProductProcesses, | fetchProductProcesses, | ||||
| fetchProductProcessesByJobOrderId, | fetchProductProcessesByJobOrderId, | ||||
| @@ -165,7 +167,9 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}> | ||||
| <Tab label={t("Production Process")} /> | <Tab label={t("Production Process")} /> | ||||
| <Tab label={t("Finished QC Job Orders")} /> | <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> | </Tabs> | ||||
| {tabIndex === 0 && ( | {tabIndex === 0 && ( | ||||
| @@ -195,6 +199,12 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| {tabIndex === 2 && ( | {tabIndex === 2 && ( | ||||
| <JobProcessStatus /> | <JobProcessStatus /> | ||||
| )} | )} | ||||
| {tabIndex === 3 && ( | |||||
| <OperatorKpiDashboard /> | |||||
| )} | |||||
| {tabIndex === 4 && ( | |||||
| <EquipmentStatusDashboard /> | |||||
| )} | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -76,7 +76,8 @@ const SubmitIssueForm: React.FC<Props> = ({ | |||||
| }; | }; | ||||
| const handleSubmit = async () => { | const handleSubmit = async () => { | ||||
| if (!lotId || !submitQty || parseFloat(submitQty) <= 0) { | |||||
| if (!lotId || !submitQty || parseFloat(submitQty) < 0) { | |||||
| alert(t("Please enter a valid quantity")); | alert(t("Please enter a valid quantity")); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -175,7 +176,7 @@ const SubmitIssueForm: React.FC<Props> = ({ | |||||
| <Button | <Button | ||||
| onClick={handleSubmit} | onClick={handleSubmit} | ||||
| variant="contained" | variant="contained" | ||||
| disabled={submitting || !submitQty || parseFloat(submitQty) <= 0} | |||||
| disabled={submitting || !submitQty || parseFloat(submitQty) < 0} | |||||
| > | > | ||||
| {submitting ? t("Submitting...") : t("Submit")} | {submitting ? t("Submitting...") : t("Submit")} | ||||
| </Button> | </Button> | ||||
| @@ -15,7 +15,7 @@ | |||||
| "Search": "搜索", | "Search": "搜索", | ||||
| "This lot is rejected, please scan another lot.": "此批次已封存,請掃描另一個批號。", | "This lot is rejected, please scan another lot.": "此批次已封存,請掃描另一個批號。", | ||||
| "Process Start Time": "工序開始時間", | "Process Start Time": "工序開始時間", | ||||
| "Stock Req. Qty": "需求數(庫存單位)", | |||||
| "Stock Req. Qty": "需求數", | |||||
| "Staff No Required": "員工編號必填", | "Staff No Required": "員工編號必填", | ||||
| "User Not Found": "用戶不存在", | "User Not Found": "用戶不存在", | ||||
| "Time Remaining": "剩餘時間", | "Time Remaining": "剩餘時間", | ||||
| @@ -52,7 +52,7 @@ | |||||
| "No": "沒有", | "No": "沒有", | ||||
| "Assignment failed: ": "分配失敗: ", | "Assignment failed: ": "分配失敗: ", | ||||
| "Unknown error": "未知錯誤", | "Unknown error": "未知錯誤", | ||||
| "Job Process Status": "工單流程狀態", | |||||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | |||||
| "Total Time": "總時間", | "Total Time": "總時間", | ||||
| "Remaining Time": "剩餘時間", | "Remaining Time": "剩餘時間", | ||||
| @@ -94,7 +94,7 @@ | |||||
| "Deliver Order": "送貨訂單", | "Deliver Order": "送貨訂單", | ||||
| "Project": "專案", | "Project": "專案", | ||||
| "Product": "產品", | "Product": "產品", | ||||
| "Material": "材料", | |||||
| "mat": "原料", | "mat": "原料", | ||||
| "consumables": "消耗品", | "consumables": "消耗品", | ||||
| "non-consumables": "非消耗品", | "non-consumables": "非消耗品", | ||||
| @@ -109,7 +109,7 @@ | |||||
| "Detail Scheduling": "詳細排程", | "Detail Scheduling": "詳細排程", | ||||
| "Customer": "客戶", | "Customer": "客戶", | ||||
| "qcItem": "品檢項目", | "qcItem": "品檢項目", | ||||
| "Item": "成品/半成品", | |||||
| "Today": "今天", | "Today": "今天", | ||||
| "Yesterday": "昨天", | "Yesterday": "昨天", | ||||
| "Input Equipment is not match with process": "輸入的設備與流程不匹配", | "Input Equipment is not match with process": "輸入的設備與流程不匹配", | ||||
| @@ -209,9 +209,10 @@ | |||||
| "Row per page": "每頁行數", | "Row per page": "每頁行數", | ||||
| "Select Unit": "選擇單位", | "Select Unit": "選擇單位", | ||||
| "No data available": "沒有資料", | "No data available": "沒有資料", | ||||
| "Bom Req. Qty": "需求數(BOM單位)", | |||||
| "Bom Req. Qty": "BOM", | |||||
| "Material Name": "材料清單", | "Material Name": "材料清單", | ||||
| "Material Code": "材料清單", | "Material Code": "材料清單", | ||||
| "Bom UOM": "使用單位", | |||||
| "Base UOM": "基本單位", | "Base UOM": "基本單位", | ||||
| "Stock UOM": "庫存單位", | "Stock UOM": "庫存單位", | ||||
| "jodetail": "工單細節", | "jodetail": "工單細節", | ||||
| @@ -238,7 +239,7 @@ | |||||
| "Is Dense": "濃淡", | "Is Dense": "濃淡", | ||||
| "Is Float": "浮沉", | "Is Float": "浮沉", | ||||
| "Job Order Code": "工單編號", | "Job Order Code": "工單編號", | ||||
| "Operator": "操作員", | |||||
| "Output Qty": "輸出數量", | "Output Qty": "輸出數量", | ||||
| "Pending": "待處理", | "Pending": "待處理", | ||||
| "pending": "待處理", | "pending": "待處理", | ||||
| @@ -264,10 +265,10 @@ | |||||
| "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | "Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.", | ||||
| "View": "查看", | "View": "查看", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "BoM Material": "成品/半成品清單", | |||||
| "BoM Material": "材料清單", | |||||
| "N/A": "不適用", | "N/A": "不適用", | ||||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度", | "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度", | ||||
| "Item Code": "成品/半成品名稱", | |||||
| "Item Code": "材料名稱", | |||||
| "Please scan equipment code": "請掃描設備編號", | "Please scan equipment code": "請掃描設備編號", | ||||
| "Equipment Code": "設備編號", | "Equipment Code": "設備編號", | ||||
| "Seq": "步驟", | "Seq": "步驟", | ||||
| @@ -283,7 +284,7 @@ | |||||
| "Seq No": "加入步驟", | "Seq No": "加入步驟", | ||||
| "Total pick orders": "總提料單數量", | "Total pick orders": "總提料單數量", | ||||
| "Seq No Remark": "序號明細", | "Seq No Remark": "序號明細", | ||||
| "Stock Available": "庫存可用", | |||||
| "Stock Available": "庫存數", | |||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Do you want to delete?": "您確定要刪除嗎?", | "Do you want to delete?": "您確定要刪除嗎?", | ||||
| "Stock Status": "庫存狀態", | "Stock Status": "庫存狀態", | ||||
| @@ -300,6 +301,7 @@ | |||||
| "update production priority": "更新生產優先序", | "update production priority": "更新生產優先序", | ||||
| "Assume Time Need": "預計所需時間", | "Assume Time Need": "預計所需時間", | ||||
| "Required Qty": "需求數", | "Required Qty": "需求數", | ||||
| "Bom Required Qty": "Bom 使用份量", | |||||
| "Total processes": "總流程數", | "Total processes": "總流程數", | ||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "view stockin": "品檢", | "view stockin": "品檢", | ||||
| @@ -412,6 +414,22 @@ | |||||
| "Equipment Code": "設備編號", | "Equipment Code": "設備編號", | ||||
| "Yes": "是", | "Yes": "是", | ||||
| "No": "否", | "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": "更新設備的維護和保養", | "Update Equipment Maintenance and Repair": "更新設備的維護和保養", | ||||
| "Equipment Information": "設備資訊", | "Equipment Information": "設備資訊", | ||||
| "Loading": "載入中...", | "Loading": "載入中...", | ||||
| @@ -11,9 +11,12 @@ | |||||
| "Status": "來貨狀態", | "Status": "來貨狀態", | ||||
| "Order Date From": "訂單日期", | "Order Date From": "訂單日期", | ||||
| "Delivery Order Code": "送貨訂單編號", | "Delivery Order Code": "送貨訂單編號", | ||||
| "Select Remark": "選擇備註", | "Select Remark": "選擇備註", | ||||
| "Confirm Assignment": "確認分配", | "Confirm Assignment": "確認分配", | ||||
| "Required Date": "所需日期", | "Required Date": "所需日期", | ||||
| "Submit Miss Item": "提交缺貨品", | |||||
| "Submit Quantity": "提交數量", | |||||
| "Store": "位置", | "Store": "位置", | ||||
| "Lane Code": "車線號碼", | "Lane Code": "車線號碼", | ||||
| "Available Orders": "可用訂單", | "Available Orders": "可用訂單", | ||||
| @@ -33,6 +33,11 @@ | |||||
| "Start Time": "開始時間", | "Start Time": "開始時間", | ||||
| "Difference": "差異", | "Difference": "差異", | ||||
| "stockTaking": "盤點中", | "stockTaking": "盤點中", | ||||
| "Pick Order Code": "提料單編號", | |||||
| "DO Order Code": "送貨單編號", | |||||
| "JO Order Code": "工單編號", | |||||
| "Picker Name": "提料員", | |||||
| "rejected": "已拒絕", | "rejected": "已拒絕", | ||||
| "miss": "缺貨", | "miss": "缺貨", | ||||
| "bad": "不良", | "bad": "不良", | ||||
| @@ -11,11 +11,11 @@ | |||||
| "Picked Qty": "已提料數量", | "Picked Qty": "已提料數量", | ||||
| "Confirm All": "確認所有提料", | "Confirm All": "確認所有提料", | ||||
| "Wait Time [minutes]": "等待時間(分鐘)", | "Wait Time [minutes]": "等待時間(分鐘)", | ||||
| "Job Process Status": "工單流程狀態", | |||||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | |||||
| "This lot is rejected, please scan another lot.": "此批次已拒收,請掃描另一個批次。", | "This lot is rejected, please scan another lot.": "此批次已拒收,請掃描另一個批次。", | ||||
| "Edit": "改數", | "Edit": "改數", | ||||
| "Just Complete": "已完成", | "Just Complete": "已完成", | ||||
| "Stock Req. Qty": "需求數(庫存單位)", | |||||
| "Stock Req. Qty": "需求數", | |||||
| "Bad Package Qty": "不良包裝數量", | "Bad Package Qty": "不良包裝數量", | ||||
| "Progress": "進度", | "Progress": "進度", | ||||
| "Search Job Order/ Create Job Order":"搜尋工單/建立工單", | "Search Job Order/ Create Job Order":"搜尋工單/建立工單", | ||||
| @@ -100,7 +100,7 @@ | |||||
| "Pause Reason": "暫停原因", | "Pause Reason": "暫停原因", | ||||
| "Bag Usage": "包裝袋使用記錄", | "Bag Usage": "包裝袋使用記錄", | ||||
| "Reason": "原因", | "Reason": "原因", | ||||
| "Stock Available": "倉庫可用數", | |||||
| "update production priority": "更新生產優先序", | "update production priority": "更新生產優先序", | ||||
| "Staff No": "員工編號", | "Staff No": "員工編號", | ||||
| "Please scan staff no": "請掃描員工編號", | "Please scan staff no": "請掃描員工編號", | ||||
| @@ -108,7 +108,7 @@ | |||||
| "Total lines: ": "所需貨品項目數量: ", | "Total lines: ": "所需貨品項目數量: ", | ||||
| "Lines with sufficient stock: ": "可提料項目數量: ", | "Lines with sufficient stock: ": "可提料項目數量: ", | ||||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | "Lines with insufficient stock: ": "未能提料項目數量: ", | ||||
| "Item Name": "成品/半成品", | |||||
| "Item Name": "材料名稱", | |||||
| "Material Code": "材料編號", | "Material Code": "材料編號", | ||||
| "Select Unit": "選擇單位", | "Select Unit": "選擇單位", | ||||
| "Job Order Pickexcution": "工單提料", | "Job Order Pickexcution": "工單提料", | ||||
| @@ -212,7 +212,8 @@ | |||||
| "No Group": "沒有組", | "No Group": "沒有組", | ||||
| "No created items": "沒有創建物料", | "No created items": "沒有創建物料", | ||||
| "Order Quantity": "需求數", | "Order Quantity": "需求數", | ||||
| "Bom Req. Qty": "需求數(BOM單位)", | |||||
| "Bom Req. Qty": "BOM", | |||||
| "Bom Uom": "使用單位", | |||||
| "Selected": "已選擇", | "Selected": "已選擇", | ||||
| "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", | "Are you sure you want to delete this procoess?": "您確定要刪除此工序嗎?", | ||||
| "Please select item": "請選擇物料", | "Please select item": "請選擇物料", | ||||
| @@ -421,7 +422,7 @@ | |||||
| "View": "查看", | "View": "查看", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| "N/A": "不適用", | "N/A": "不適用", | ||||
| "BoM Material": "成品/半成品清單", | |||||
| "BoM Material": "材料清單", | |||||
| "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | "Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度", | ||||
| "Enter the number of cartons: ": "請輸入箱數:", | "Enter the number of cartons: ": "請輸入箱數:", | ||||
| "Number of cartons": "箱數", | "Number of cartons": "箱數", | ||||
| @@ -443,7 +444,7 @@ | |||||
| "Req. Qty": "需求數量", | "Req. Qty": "需求數量", | ||||
| "Seq No": "加入步驟", | "Seq No": "加入步驟", | ||||
| "Seq No Remark": "序號明細", | "Seq No Remark": "序號明細", | ||||
| "Stock Available": "庫存可用", | |||||
| "Stock Available": "庫存數", | |||||
| "Stock Status": "庫存狀態", | "Stock Status": "庫存狀態", | ||||
| "Target Production Date": "目標生產日期", | "Target Production Date": "目標生產日期", | ||||
| "Description": "描述", | "Description": "描述", | ||||
| @@ -536,13 +537,22 @@ | |||||
| "Finished Good Order": "成品出倉", | "Finished Good Order": "成品出倉", | ||||
| "finishedGood": "成品", | "finishedGood": "成品", | ||||
| "Router": "執貨路線", | "Router": "執貨路線", | ||||
| "Equipment Name and Code": "設備名稱及編號", | |||||
| "Remaining Time (min)": "剩餘時間(分鐘)", | |||||
| "Idle": "閒置", | |||||
| "Repair": "維修", | |||||
| "Production Equipment Status Dashboard": "儀表板 - 生產設備狀態", | |||||
| "Operator KPI Dashboard": "儀表板 - 操作員KPI概覽", | |||||
| "Start Scan": "開始掃碼", | "Start Scan": "開始掃碼", | ||||
| "Stop Scan": "停止掃碼", | "Stop Scan": "停止掃碼", | ||||
| "Operator Name & No.": "操作員名稱及編號", | |||||
| "Count of Job Orders": "工單數量", | |||||
| "Total Processing Time": "總生產時間", | |||||
| "Material Pick Status": "物料提料狀態", | "Material Pick Status": "物料提料狀態", | ||||
| "Job Order Qty": "工單數量", | "Job Order Qty": "工單數量", | ||||
| "Sign out": "登出", | "Sign out": "登出", | ||||
| "Job Order No.": "工單編號", | "Job Order No.": "工單編號", | ||||
| "Operator KPI": "操作員KPI", | |||||
| "FG / WIP Item": "成品/半成品", | "FG / WIP Item": "成品/半成品", | ||||
| "Production Time Remaining": "生產剩餘時間", | "Production Time Remaining": "生產剩餘時間", | ||||
| "Process": "工序", | "Process": "工序", | ||||