| @@ -906,7 +906,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (type?: string | n | |||
| ); | |||
| }); | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||
| export const fetchJoborderProductProcessesPage = cache(async (params: { | |||
| /** Job order / process date(YYYY-MM-DD) */ | |||
| date?: string | null; | |||
| @@ -924,4 +924,78 @@ export const importTruckLaneScheduleExcelAction = async ( | |||
| ); | |||
| } | |||
| return (await response.json()) as TruckLaneScheduleResponse[]; | |||
| }; | |||
| export interface PreviewTruckLaneRenameRequest { | |||
| truckLanceCode: string; | |||
| storeId: string; | |||
| remark?: string | null; | |||
| } | |||
| export interface PreviewTruckLaneRenameResponse { | |||
| unfinishedCount: number; | |||
| completedCount: number; | |||
| handlerNames: string[]; | |||
| openScheduleCount: number; | |||
| } | |||
| export interface RenameTruckLaneRequest { | |||
| truckLanceCode: string; | |||
| storeId: string; | |||
| remark?: string | null; | |||
| newTruckLanceCode: string; | |||
| departureTimeChanged: boolean; | |||
| newDepartureTime?: string | null; | |||
| } | |||
| export interface RenameTruckLaneResponse { | |||
| truckLanceCode: string; | |||
| ticketCount: number; | |||
| scheduleLineCount: number; | |||
| } | |||
| export interface TruckLaneRenameLogResponse { | |||
| id: number; | |||
| created?: string | null; | |||
| createdBy?: string | null; | |||
| oldCode: string; | |||
| newCode: string; | |||
| storeId: string; | |||
| remark?: string | null; | |||
| oldDepartureTime?: string | null; | |||
| newDepartureTime?: string | null; | |||
| ticketCount: number; | |||
| /** `truck.id` of shops in the renamed lane, comma-separated. */ | |||
| truckIds?: string | null; | |||
| } | |||
| export const previewTruckLaneRenameAction = async ( | |||
| data: PreviewTruckLaneRenameRequest, | |||
| ) => { | |||
| return serverFetchJson<PreviewTruckLaneRenameResponse>( | |||
| `${BASE_API_URL}/truck/previewTruckLaneRename`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| }; | |||
| export const renameTruckLaneAction = async (data: RenameTruckLaneRequest) => { | |||
| return serverFetchJson<RenameTruckLaneResponse>( | |||
| `${BASE_API_URL}/truck/renameTruckLane`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| }; | |||
| export const listTruckLaneRenameLogsAction = async () => { | |||
| return serverFetchJson<TruckLaneRenameLogResponse[]>( | |||
| `${BASE_API_URL}/truck/truckLaneRenameLog`, | |||
| { method: "GET", headers: { "Content-Type": "application/json" } }, | |||
| ); | |||
| }; | |||
| @@ -66,7 +66,12 @@ import { | |||
| type SaveTruckRequest, | |||
| type UpdateTruckShopDetailsRequest, | |||
| type CreateTruckWithoutShopRequest, | |||
| type MessageResponse | |||
| type MessageResponse, | |||
| previewTruckLaneRenameAction, | |||
| renameTruckLaneAction, | |||
| listTruckLaneRenameLogsAction, | |||
| type PreviewTruckLaneRenameRequest, | |||
| type RenameTruckLaneRequest, | |||
| } from "./actions"; | |||
| export const fetchAllShopsClient = async (params?: Record<string, string | number | null>) => { | |||
| @@ -276,6 +281,20 @@ export const parseTruckLaneScheduleExcelClient = async ( | |||
| return await parseTruckLaneScheduleExcelAction(formData, defaultExecuteAt); | |||
| }; | |||
| export const previewTruckLaneRenameClient = async ( | |||
| data: PreviewTruckLaneRenameRequest, | |||
| ) => { | |||
| return await previewTruckLaneRenameAction(data); | |||
| }; | |||
| export const renameTruckLaneClient = async (data: RenameTruckLaneRequest) => { | |||
| return await renameTruckLaneAction(data); | |||
| }; | |||
| export const listTruckLaneRenameLogsClient = async () => { | |||
| return await listTruckLaneRenameLogsAction(); | |||
| }; | |||
| export const importTruckLaneScheduleExcelClient = async ( | |||
| formData: FormData, | |||
| defaultExecuteAt?: string | null, | |||
| @@ -203,13 +203,13 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| return '-'; | |||
| }; | |||
| // Compare a timestamp against the selected date (today/tomorrow/...) and return a short label. | |||
| // Used to avoid confusion when picking crosses midnight. | |||
| // Compare a timestamp against calendar today. Same-day picks show no chip; | |||
| // only midnight-crossing (yesterday / tomorrow / N days) gets a label. | |||
| // Does not filter rows — trucklane tickets for the selected delivery date still show. | |||
| const relativeDayLabel = useCallback( | |||
| (dateTimeData: string | number[] | null, dateOption: string): string | null => { | |||
| (dateTimeData: string | number[] | null): string | null => { | |||
| if (!dateTimeData) return null; | |||
| const dateOffset = getDateOffset(dateOption); | |||
| const baseDate = dayjs().startOf('day').add(dateOffset, 'day'); | |||
| const baseDate = dayjs().startOf('day'); | |||
| let dt: dayjs.Dayjs; | |||
| if (Array.isArray(dateTimeData)) { | |||
| @@ -518,8 +518,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| filteredData.map((row, index) => { | |||
| const timeRemaining = calculateTimeRemaining(row, selectedDate); | |||
| const chipColor = getTimeChipColor(row, selectedDate); | |||
| const startRel = relativeDayLabel(row.firstTicketStartTime, selectedDate); | |||
| const endRel = relativeDayLabel(row.lastTicketEndTime, selectedDate); | |||
| const startRel = relativeDayLabel(row.firstTicketStartTime); | |||
| const endRel = relativeDayLabel(row.lastTicketEndTime); | |||
| return ( | |||
| <TableRow | |||
| @@ -91,6 +91,12 @@ export interface WorkbenchLotLabelPrintModalProps { | |||
| /** 與 statusTitleText 搭配;預設 error(舊版固定紅字) */ | |||
| statusTitleSeverity?: "success" | "warning" | "error"; | |||
| warehouseCodePrefixFilter?: string; | |||
| /** | |||
| * Hide lots whose warehouse code equals an entry or starts with `{entry}-`. | |||
| * JO passes `4F` so the pick-station list matches the suggestion block list. | |||
| * The row that opened the dialog stays visible. | |||
| */ | |||
| excludeWarehouseCodePrefixes?: string[]; | |||
| /** | |||
| * When true, omit the API 「scanned」 lot from the merged list (legacy FG-style). | |||
| * Workbench should leave false so the current row’s lot appears for label print / scan-pick. | |||
| @@ -147,7 +153,7 @@ function isLabelPrinter(p: Printer): boolean { | |||
| return s.includes("label") && !s.includes("a4"); | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.1 | 2026-07-22 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.2 | 2026-11-02 */ | |||
| const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ | |||
| open, | |||
| onClose, | |||
| @@ -159,6 +165,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| statusTitleText, | |||
| statusTitleSeverity = "error", | |||
| warehouseCodePrefixFilter, | |||
| excludeWarehouseCodePrefixes, | |||
| hideTriggeredLot = false, | |||
| triggerLotAvailableQty = null, | |||
| triggerLotUom = null, | |||
| @@ -450,14 +457,22 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| ]); | |||
| const filteredLots = useMemo(() => { | |||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | |||
| if (!prefix) return availableLots; | |||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim().toUpperCase(); | |||
| const blocked = (excludeWarehouseCodePrefixes ?? []) | |||
| .map((entry) => entry.trim().toUpperCase()) | |||
| .filter((entry) => entry.length > 0); | |||
| return availableLots.filter((lot) => { | |||
| // 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號 | |||
| // 使用者從本列開啟視窗:即使倉位在排除清單,仍顯示目前這筆批號 | |||
| if (lot._scanned) return true; | |||
| return String(lot.warehouseCode ?? "").startsWith(prefix); | |||
| const code = String(lot.warehouseCode ?? "").trim().toUpperCase(); | |||
| const blockedHit = blocked.some( | |||
| (entry) => code === entry || code.startsWith(`${entry}-`), | |||
| ); | |||
| if (blockedHit) return false; | |||
| if (!prefix) return true; | |||
| return code.startsWith(prefix); | |||
| }); | |||
| }, [availableLots, warehouseCodePrefixFilter]); | |||
| }, [availableLots, warehouseCodePrefixFilter, excludeWarehouseCodePrefixes]); | |||
| const selectedPrinter = useMemo(() => { | |||
| if (selectedPrinterId === "") return null; | |||
| @@ -618,7 +618,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.1 | 2026-08-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.2 | 2026-11-02 */ | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | |||
| const workbenchMode = true; | |||
| const { t } = useTranslation("jo"); | |||
| @@ -4642,6 +4642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| workbenchLotLabelContextLot != null | |||
| } | |||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | |||
| excludeWarehouseCodePrefixes={["4F"]} | |||
| triggerLotAvailableQty={ | |||
| workbenchLotLabelContextLot != null | |||
| ? Number(workbenchLotLabelContextLot.availableQty) | |||
| @@ -251,7 +251,7 @@ const NavigationContent: React.FC = () => { | |||
| icon: <ShowChart />, | |||
| labelKey: "nav.chartReports", | |||
| path: "", | |||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN], | |||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN, AUTH.PERFORMANCE], | |||
| isHidden: false, | |||
| children: [ | |||
| { | |||
| @@ -280,7 +280,7 @@ const NavigationContent: React.FC = () => { | |||
| icon: <LocalShipping />, | |||
| labelKey: "nav.chart.delivery", | |||
| path: "/chart/delivery", | |||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN], | |||
| requiredAbility: [AUTH.TESTING, AUTH.ADMIN, AUTH.PERFORMANCE], | |||
| }, | |||
| { | |||
| id: "nav.chart.warehouse", | |||
| @@ -17,25 +17,42 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { UpdateProductProcessLineQtyRequest } from "@/app/api/jo/actions"; | |||
| type QtyInput = number | ""; | |||
| export type ProductionOutputFormData = Omit< | |||
| UpdateProductProcessLineQtyRequest, | |||
| | "outputFromProcessQty" | |||
| | "byproductQty" | |||
| | "defectQty" | |||
| | "defect2Qty" | |||
| | "defect3Qty" | |||
| | "scrapQty" | |||
| > & { | |||
| outputFromProcessQty: QtyInput; | |||
| byproductQty: QtyInput; | |||
| defectQty: QtyInput; | |||
| defect2Qty: QtyInput; | |||
| defect3Qty: QtyInput; | |||
| scrapQty: QtyInput; | |||
| }; | |||
| const parseQtyInput = (raw: string): QtyInput => { | |||
| if (raw === "") return ""; | |||
| const n = parseInt(raw, 10); | |||
| return Number.isNaN(n) ? "" : n; | |||
| }; | |||
| export const toSubmitQty = (v: QtyInput | null | undefined): number => | |||
| v === "" || v == null || Number.isNaN(Number(v)) ? 0 : Number(v); | |||
| interface ProductionOutputFormProps { | |||
| outputData: (UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| }); | |||
| setOutputData: React.Dispatch< | |||
| React.SetStateAction< | |||
| UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| } | |||
| > | |||
| >; | |||
| outputData: ProductionOutputFormData; | |||
| setOutputData: React.Dispatch<React.SetStateAction<ProductionOutputFormData>>; | |||
| onSubmit: () => Promise<void> | void; | |||
| onCancel: () => void; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||
| const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| outputData, | |||
| setOutputData, | |||
| @@ -68,7 +85,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessQty: parseInt(e.target.value) || 0, | |||
| outputFromProcessQty: parseQtyInput(e.target.value), | |||
| }) | |||
| } | |||
| /> | |||
| @@ -103,7 +120,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| byproductQty: parseInt(e.target.value) || 0, | |||
| byproductQty: parseQtyInput(e.target.value), | |||
| }) | |||
| } | |||
| /> | |||
| @@ -138,7 +155,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| defectQty: parseInt(e.target.value) || 0, | |||
| defectQty: parseQtyInput(e.target.value), | |||
| }) | |||
| } | |||
| /> | |||
| @@ -173,7 +190,7 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| onChange={(e) => | |||
| setOutputData({ | |||
| ...outputData, | |||
| scrapQty: parseInt(e.target.value) || 0, | |||
| scrapQty: parseQtyInput(e.target.value), | |||
| }) | |||
| } | |||
| /> | |||
| @@ -207,4 +224,4 @@ const ProductionOutputForm: React.FC<ProductionOutputFormProps> = ({ | |||
| ); | |||
| }; | |||
| export default ProductionOutputForm; | |||
| export default ProductionOutputForm; | |||
| @@ -5,47 +5,47 @@ import { | |||
| Button, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| fetchProductProcessLineDetail, | |||
| updateProductProcessLineQty, | |||
| UpdateProductProcessLineQtyRequest, | |||
| JobOrderProcessLineDetailResponse, | |||
| } from "@/app/api/jo/actions"; | |||
| import ProductionOutputForm from "./ProductionOutputForm"; | |||
| import ProductionOutputForm, { | |||
| ProductionOutputFormData, | |||
| toSubmitQty, | |||
| } from "./ProductionOutputForm"; | |||
| interface ProductionOutputFormPageProps { | |||
| lineId: number | null; | |||
| onBack: () => void; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||
| const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| lineId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { t } = useTranslation(["common", "jo", "productionProcess"]); | |||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| }>({ | |||
| const [outputData, setOutputData] = useState<ProductionOutputFormData>({ | |||
| productProcessLineId: lineId ?? 0, | |||
| outputFromProcessQty: 0, | |||
| outputFromProcessQty: "", | |||
| outputFromProcessUom: "", | |||
| defectQty: 0, | |||
| defectQty: "", | |||
| defectUom: "", | |||
| defect2Qty: 0, | |||
| defect2Qty: "", | |||
| defect2Uom: "", | |||
| defect3Qty: 0, | |||
| defect3Qty: "", | |||
| defect3Uom: "", | |||
| defectDescription: "", | |||
| defectDescription2: "", | |||
| defectDescription3: "", | |||
| scrapQty: 0, | |||
| scrapQty: "", | |||
| scrapUom: "", | |||
| byproductName: "", | |||
| byproductQty: 0, | |||
| byproductQty: "", | |||
| byproductUom: "", | |||
| }); | |||
| @@ -60,14 +60,14 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| setOutputData((prev) => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectQty: detail.defectQty || "", | |||
| defectUom: detail.defectUom || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| scrapQty: detail.scrapQty || "", | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductQty: detail.byproductQty || "", | |||
| byproductUom: detail.byproductUom || "", | |||
| })); | |||
| }) | |||
| @@ -84,6 +84,12 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| await updateProductProcessLineQty({ | |||
| ...outputData, | |||
| productProcessLineId: lineDetail.id || 0, | |||
| outputFromProcessQty: toSubmitQty(outputData.outputFromProcessQty), | |||
| byproductQty: toSubmitQty(outputData.byproductQty), | |||
| defectQty: toSubmitQty(outputData.defectQty), | |||
| defect2Qty: toSubmitQty(outputData.defect2Qty), | |||
| defect3Qty: toSubmitQty(outputData.defect3Qty), | |||
| scrapQty: toSubmitQty(outputData.scrapQty), | |||
| }); | |||
| console.log("Output data submitted successfully"); | |||
| @@ -94,14 +100,14 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| setOutputData((prev) => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectQty: detail.defectQty || "", | |||
| defectUom: detail.defectUom || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| scrapQty: detail.scrapQty || "", | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductQty: detail.byproductQty || "", | |||
| byproductUom: detail.byproductUom || "", | |||
| })); | |||
| @@ -116,8 +122,13 @@ const ProductionOutputFormPage: React.FC<ProductionOutputFormPageProps> = ({ | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={onBack} | |||
| startIcon={<ArrowBackIcon />} | |||
| > | |||
| {t("Back to Process Steps")} | |||
| </Button> | |||
| </Box> | |||
| @@ -64,13 +64,16 @@ interface ProductProcessDetailProps { | |||
| jobOrderId: number; | |||
| onBack: () => void; | |||
| fromJosave?: boolean; | |||
| /** 進入步驟執行頁時通知父層(用來隱藏「返回列表」) */ | |||
| onStepViewChange?: (inStepView: boolean) => void; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.1 | 2026-08-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.2 | 2026-10-29 */ | |||
| const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| jobOrderId, | |||
| onBack, | |||
| fromJosave, | |||
| onStepViewChange, | |||
| }) => { | |||
| // console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); | |||
| @@ -143,6 +146,12 @@ const fetchProcessDetailRef = useRef<() => Promise<void>>(); | |||
| useEffect(() => { | |||
| onBackRef.current = onBack; | |||
| }, [onBack]); | |||
| useEffect(() => { | |||
| const inStepView = isExecutingLine && selectedLineId != null; | |||
| onStepViewChange?.(inStepView); | |||
| return () => onStepViewChange?.(false); | |||
| }, [isExecutingLine, selectedLineId, onStepViewChange]); | |||
| // 获取 process 和 lines 数据 | |||
| const fetchProcessDetail = useCallback(async () => { | |||
| // console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); | |||
| @@ -54,7 +54,7 @@ interface ProductProcessJobOrderDetailProps { | |||
| initialTabIndex?: number; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.7 | 2026-08-06 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||
| const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({ | |||
| jobOrderId, | |||
| onBack, | |||
| @@ -68,6 +68,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [tabIndex, setTabIndex] = useState(initialTabIndex); | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| const [inStepView, setInStepView] = useState(false); | |||
| const [operationPriority, setOperationPriority] = useState<number>(50); | |||
| const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | |||
| const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); | |||
| @@ -320,6 +321,9 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| if (newValue !== 2) { | |||
| setInStepView(false); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| @@ -796,12 +800,14 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||
| return ( | |||
| <Box> | |||
| {/* 返回按钮 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* 步驟執行頁不顯示「返回列表」,避免誤離開工單 */} | |||
| {!inStepView && ( | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| )} | |||
| {/* 标签页 */} | |||
| <Box sx={{ borderBottom: '1px solid #e0e0e0' }}> | |||
| @@ -833,6 +839,7 @@ const handleRelease = useCallback(async (jobOrderId: number) => { | |||
| }} | |||
| fromJosave={Boolean(fromJosave && !isPutAwayed)} | |||
| onStepViewChange={setInStepView} | |||
| /> | |||
| )} | |||
| {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />} | |||
| @@ -29,6 +29,7 @@ import { | |||
| import ChevronLeft from "@mui/icons-material/ChevronLeft"; | |||
| import ChevronRight from "@mui/icons-material/ChevronRight"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import type { StockInLine } from "@/app/api/stockIn"; | |||
| import { fetchItemForPutAway } from "@/app/api/stockIn/actions"; | |||
| import QcStockInModal from "../Qc/QcStockInModal"; | |||
| import { useSession } from "next-auth/react"; | |||
| @@ -178,7 +179,7 @@ function isWaitingQcPutAway( | |||
| return s !== "completed" && s !== "rejected"; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.9 | 2026-08-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.11 | 2026-10-29 */ | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| onSelectProcess, | |||
| printerCombo, | |||
| @@ -191,6 +192,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const sessionToken = session as SessionWithTokens | null; | |||
| const [loading, setLoading] = useState(false); | |||
| /** Bumps on Search/Reset so the same criteria still refetch. */ | |||
| const [listReloadKey, setListReloadKey] = useState(0); | |||
| const [productionCache, setProductionCache] = useState< | |||
| AllJoborderProductProcessInfoResponse[] | |||
| >([]); | |||
| @@ -234,6 +237,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| () => printerOptions[0] ?? null, | |||
| ); | |||
| const printInFlightRef = useRef(false); | |||
| const loadSeqRef = useRef(0); | |||
| const hasLoadedOnceRef = useRef(false); | |||
| const [printingSilId, setPrintingSilId] = useState<number | null>(null); | |||
| useEffect(() => { | |||
| @@ -328,6 +333,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| const handleApplySearch = useCallback( | |||
| (inputs: Record<SearchParam | `${SearchParam}To`, string>) => { | |||
| const selectedProcessType = (inputs.processType || "all") as ProcessFilter; | |||
| setListReloadKey((k) => k + 1); | |||
| onListPersistedStateChange((prev) => ({ | |||
| ...prev, | |||
| filter: selectedProcessType, | |||
| @@ -342,6 +348,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| ); | |||
| const handleResetSearch = useCallback(() => { | |||
| setListReloadKey((k) => k + 1); | |||
| onListPersistedStateChange((prev) => ({ | |||
| ...prev, | |||
| filter: "all", | |||
| @@ -355,8 +362,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| }, [disableDateFilter, onListPersistedStateChange]); | |||
| /** Load all tab datasets once per search; tab switches use cache only. */ | |||
| const loadAllCaches = useCallback(async () => { | |||
| setLoading(true); | |||
| const loadAllCaches = useCallback(async (options?: { silent?: boolean }) => { | |||
| const silent = options?.silent === true; | |||
| const seq = ++loadSeqRef.current; | |||
| if (!silent) setLoading(true); | |||
| try { | |||
| const typeParam = filter === "all" ? undefined : filter; | |||
| const base = { | |||
| @@ -391,6 +400,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| }), | |||
| ]); | |||
| if (seq !== loadSeqRef.current) return; | |||
| const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p)); | |||
| const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p)); | |||
| const putawayContent = putawayed?.content || []; | |||
| @@ -405,6 +416,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| setCarriedOverCount(prod?.carriedOverCount ?? 0); | |||
| } catch (e) { | |||
| console.error(e); | |||
| if (seq !== loadSeqRef.current || silent) return; | |||
| setProductionCache([]); | |||
| setPendingQcCache([]); | |||
| setPutawayedCache([]); | |||
| @@ -413,12 +425,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| setPutawayedCount(0); | |||
| setCarriedOverCount(0); | |||
| } finally { | |||
| setLoading(false); | |||
| if (seq === loadSeqRef.current) setLoading(false); | |||
| } | |||
| }, [appliedSearch, disableDateFilter, filter]); | |||
| }, [appliedSearch, disableDateFilter, filter, listReloadKey]); | |||
| useEffect(() => { | |||
| void loadAllCaches(); | |||
| const silent = hasLoadedOnceRef.current; | |||
| hasLoadedOnceRef.current = true; | |||
| void loadAllCaches(silent ? { silent: true } : undefined); | |||
| }, [loadAllCaches]); | |||
| const offPlanRows = useMemo(() => { | |||
| @@ -670,9 +684,12 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| closeConfirm(); | |||
| } | |||
| }, [pendingConfirmAction, closeConfirm]); | |||
| const closeNewModal = useCallback(() => { | |||
| const closeNewModal = useCallback((updatedStockInLine?: StockInLine) => { | |||
| setOpenModal(false); | |||
| }, []); | |||
| if (updatedStockInLine) { | |||
| void loadAllCaches({ silent: true }); | |||
| } | |||
| }, [loadAllCaches]); | |||
| const handlePrintLabel = useCallback( | |||
| async (process: AllJoborderProductProcessInfoResponse) => { | |||
| @@ -28,6 +28,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import StopIcon from "@mui/icons-material/Stop"; | |||
| import PauseIcon from "@mui/icons-material/Pause"; | |||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | |||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| JobOrderProcessLineDetailResponse, | |||
| @@ -47,6 +48,35 @@ import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 | |||
| import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 | |||
| import dayjs from "dayjs"; | |||
| type QtyInput = number | ""; | |||
| type OutputFormState = Omit< | |||
| UpdateProductProcessLineQtyRequest, | |||
| | "outputFromProcessQty" | |||
| | "byproductQty" | |||
| | "defectQty" | |||
| | "defect2Qty" | |||
| | "defect3Qty" | |||
| | "scrapQty" | |||
| > & { | |||
| outputFromProcessQty: QtyInput; | |||
| byproductQty: QtyInput; | |||
| defectQty: QtyInput; | |||
| defect2Qty: QtyInput; | |||
| defect3Qty: QtyInput; | |||
| scrapQty: QtyInput; | |||
| }; | |||
| const parseQtyInput = (raw: string): QtyInput => { | |||
| if (raw === "") return ""; | |||
| const n = parseInt(raw, 10); | |||
| return Number.isNaN(n) ? "" : n; | |||
| }; | |||
| const toSubmitQty = (v: QtyInput | null | undefined): number => | |||
| v === "" || v == null || Number.isNaN(Number(v)) ? 0 : Number(v); | |||
| interface ProductionProcessStepExecutionProps { | |||
| lineId: number | null; | |||
| onBack: () => void; | |||
| @@ -55,7 +85,7 @@ interface ProductionProcessStepExecutionProps { | |||
| jobOrderId?: number; // ✅ 添加 | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.0 | 2026-08-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.1 | 2026-10-29 */ | |||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | |||
| lineId, | |||
| onBack, | |||
| @@ -75,24 +105,20 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| lineStatusNorm === "autopass"; | |||
| const isPassStatus = lineStatusNorm === "pass"; | |||
| const isAutoPassStatus = lineStatusNorm === "autopass"; | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| }>({ | |||
| const [outputData, setOutputData] = useState<OutputFormState>({ | |||
| productProcessLineId: lineId ?? 0, | |||
| outputFromProcessQty: 0, | |||
| outputFromProcessQty: "", | |||
| outputFromProcessUom: "", | |||
| defectQty: 0, | |||
| defectQty: "", | |||
| defectUom: "", | |||
| scrapQty: 0, | |||
| scrapQty: "", | |||
| scrapUom: "", | |||
| byproductName: "", | |||
| byproductQty: 0, | |||
| byproductQty: "", | |||
| byproductUom: "", | |||
| defect2Qty: 0, | |||
| defect2Qty: "", | |||
| defect2Uom: "", | |||
| defect3Qty: 0, | |||
| defect3Qty: "", | |||
| defect3Uom: "", | |||
| defectDescription: "", | |||
| defectDescription2: "", | |||
| @@ -153,14 +179,14 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| setOutputData(prev => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectQty: detail.defectQty || "", | |||
| defectUom: detail.defectUom || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| scrapQty: detail.scrapQty || "", | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductQty: detail.byproductQty || "", | |||
| byproductUom: detail.byproductUom || "" | |||
| })); | |||
| }) | |||
| @@ -351,20 +377,20 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| await updateProductProcessLineQty({ | |||
| productProcessLineId: lineDetail?.id || 0 as number, | |||
| byproductName: outputData.byproductName, | |||
| byproductQty: outputData.byproductQty, | |||
| byproductQty: toSubmitQty(outputData.byproductQty), | |||
| byproductUom: outputData.byproductUom, | |||
| outputFromProcessQty: outputData.outputFromProcessQty, | |||
| outputFromProcessQty: toSubmitQty(outputData.outputFromProcessQty), | |||
| outputFromProcessUom: outputData.outputFromProcessUom, | |||
| defectQty: outputData.defectQty, | |||
| defectQty: toSubmitQty(outputData.defectQty), | |||
| defectUom: outputData.defectUom, | |||
| defect2Qty: outputData.defect2Qty, | |||
| defect2Qty: toSubmitQty(outputData.defect2Qty), | |||
| defect2Uom: outputData.defect2Uom, | |||
| defect3Qty: outputData.defect3Qty, | |||
| defect3Qty: toSubmitQty(outputData.defect3Qty), | |||
| defect3Uom: outputData.defect3Uom, | |||
| defectDescription: outputData.defectDescription, | |||
| defectDescription2: outputData.defectDescription2, | |||
| defectDescription3: outputData.defectDescription3, | |||
| scrapQty: outputData.scrapQty, | |||
| scrapQty: toSubmitQty(outputData.scrapQty), | |||
| scrapUom: outputData.scrapUom, | |||
| }); | |||
| @@ -383,21 +409,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| setOutputData(prev => ({ | |||
| ...prev, | |||
| productProcessLineId: detail.id, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || 0, | |||
| outputFromProcessQty: (detail as any).outputFromProcessQty || "", | |||
| outputFromProcessUom: (detail as any).outputFromProcessUom || "", | |||
| defectQty: detail.defectQty || 0, | |||
| defectQty: detail.defectQty || "", | |||
| defectUom: detail.defectUom || "", | |||
| defectDescription: detail.defectDescription || "", | |||
| defectDescription2: detail.defectDescription2 || "", | |||
| defectDescription3: detail.defectDescription3 || "", | |||
| defectQty2: detail.defectQty2 || 0, | |||
| defectUom2: detail.defectUom2 || "", | |||
| defectQty3: detail.defectQty3 || 0, | |||
| defectUom3: detail.defectUom3 || "", | |||
| scrapQty: detail.scrapQty || 0, | |||
| defect2Qty: (detail as any).defect2Qty || "", | |||
| defect2Uom: (detail as any).defect2Uom || "", | |||
| defect3Qty: (detail as any).defect3Qty || "", | |||
| defect3Uom: (detail as any).defect3Uom || "", | |||
| scrapQty: detail.scrapQty || "", | |||
| scrapUom: detail.scrapUom || "", | |||
| byproductName: detail.byproductName || "", | |||
| byproductQty: detail.byproductQty || 0, | |||
| byproductQty: detail.byproductQty || "", | |||
| byproductUom: detail.byproductUom || "" | |||
| })); | |||
| }) | |||
| @@ -561,8 +587,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={onBack} | |||
| startIcon={<ArrowBackIcon />} | |||
| > | |||
| {t("Back to Process Steps")} | |||
| </Button> | |||
| </Box> | |||
| {processData && ( | |||
| @@ -812,7 +843,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| value={outputData.outputFromProcessQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| outputFromProcessQty: parseInt(e.target.value) || 0 | |||
| outputFromProcessQty: parseQtyInput(e.target.value) | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| @@ -854,7 +885,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| value={outputData.defectQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defectQty: parseInt(e.target.value) || 0 | |||
| defectQty: parseQtyInput(e.target.value) | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| @@ -902,7 +933,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| value={outputData.defect2Qty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defect2Qty: parseInt(e.target.value) || 0 | |||
| defect2Qty: parseQtyInput(e.target.value) | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| @@ -950,7 +981,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| value={outputData.defect3Qty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| defect3Qty: parseInt(e.target.value) || 0 | |||
| defect3Qty: parseQtyInput(e.target.value) | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| @@ -998,7 +1029,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| value={outputData.scrapQty} | |||
| onChange={(e) => setOutputData({ | |||
| ...outputData, | |||
| scrapQty: parseInt(e.target.value) || 0 | |||
| scrapQty: parseQtyInput(e.target.value) | |||
| })} | |||
| /> | |||
| </TableCell> | |||
| @@ -77,7 +77,7 @@ interface Props extends CommonProps { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.7 | 2026-10-15 */ | |||
| const QcStockInModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| @@ -617,10 +617,10 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| } | |||
| closeWithResult(qcRes); | |||
| closeWithResult(qcRes ?? stockInLineInfo); | |||
| // setTabIndex(1); // Need to go Putaway tab? | |||
| } else { | |||
| closeWithResult(qcRes); | |||
| closeWithResult(qcRes ?? stockInLineInfo); | |||
| } | |||
| setIsSubmitting(false); | |||
| qcSubmitInFlightRef.current = false; | |||
| @@ -700,11 +700,12 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| // Handle putaway submission logic here | |||
| const res = await postStockInLine(putawayData); | |||
| console.log("result ", res); | |||
| // Close modal after successful putaway | |||
| closeHandler({}, "backdropClick"); | |||
| const updated = Array.isArray(res.entity) ? res.entity[0] : res.entity; | |||
| // Close modal after successful putaway and tell the parent to refresh. | |||
| closeWithResult(updated ?? stockInLineInfo); | |||
| }, | |||
| [closeHandler], | |||
| [closeWithResult, postStockInLine, stockInLineInfo], | |||
| ); | |||
| // Print handler | |||
| useEffect(() => { | |||
| @@ -32,6 +32,8 @@ | |||
| "BOM Type": "BOM Type", | |||
| "Back": "Back", | |||
| "Back to List": "Back to List", | |||
| "Back to Process": "Back to Process Steps", | |||
| "Back to Process Steps": "Back to Process Steps", | |||
| "Bad Item Qty": "Bad Item Qty", | |||
| "Bad Package Qty": "Bad Package Qty", | |||
| "Bag": "Bag", | |||
| @@ -10,6 +10,8 @@ | |||
| "Auto-refresh every 10 minutes": "Auto-refresh every 10 minutes", | |||
| "Back": "Back", | |||
| "Back to List": "Back to List", | |||
| "Back to Process": "Back to Process Steps", | |||
| "Back to Process Steps": "Back to Process Steps", | |||
| "Bag": "Bag", | |||
| "Bag Consumption": "Bag Consumption", | |||
| "Balance": "Balance", | |||
| @@ -106,7 +106,7 @@ | |||
| "lane_noMatchFilter": "No lanes match (clear search or floor filter)", | |||
| "floor_label": "Floor", | |||
| "floor_all": "All", | |||
| "filter_clear": "Clear", | |||
| "filter_clear": "Clear all", | |||
| "filter_apply": "OK", | |||
| "btn_addLane": "Add lane", | |||
| "tools_title": "Tools", | |||
| @@ -132,8 +132,26 @@ | |||
| "confirm_restoreDiscardsEdits": "Scheduling a version restore will discard other unsaved board changes (drags, deletes, pending shops/lanes, logistics fields, etc.). Continue?", | |||
| "confirm_restoreSaveWillDropStaging": "Save will apply the snapshot restore first; other staged edits in this save will be skipped. Continue?", | |||
| "confirm_schedule_removeShop": "Remove this shop from the lane? It will be scheduled for deletion when you submit.", | |||
| "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", | |||
| "departureDialog_title": "Edit departure time", | |||
| "departureDialog_hint": "Name and departure apply to every master row on this lane. Press \"Save changes\" to write them. Existing delivery tickets keep the old name; later releases use the new name.", | |||
| "departureDialog_title": "Edit lane", | |||
| "departureDialog_nameLabel": "Lane name", | |||
| "laneEditTooltip": "Edit lane name and departure time (written when you press Save changes)", | |||
| "confirm_weekdayToken": "The weekday token in the lane name will change or become unreadable. 4F assignment matches Mon–Sun inside the name. Apply anyway?", | |||
| "confirm_renameLane": "Rename \"{{from}}\" to \"{{to}}\".\n{{unfinished}} unfinished delivery ticket(s) stay on the old name (handlers: {{handlers}}).\n{{completed}} completed ticket(s) stay on the old name.\nLater releases use the new name.\n{{schedules}} open schedule line(s) will change from/to.\nNothing is written until you press Save changes.", | |||
| "confirm_restoreKeepsTickets": "Restore puts lane master data, and open schedules that point at the current name, back to this snapshot. Unfinished delivery tickets stay on the current name.", | |||
| "rename_noHandlers": "none", | |||
| "rename_none": "none", | |||
| "rename_savePartial": "Renamed: {{ok}}. Failed: {{fail}}. The failed lane was not written. Successful lanes stay renamed. Refresh, then save again.", | |||
| "renameErr_preview": "Could not load ticket and schedule counts, so this edit was not applied.", | |||
| "renameErr_LANE_NAME_EMPTY": "Lane name cannot be blank", | |||
| "renameErr_LANE_NAME_TOO_LONG": "Lane name is limited to 50 characters", | |||
| "renameErr_LANE_NAME_RESERVED": "\"車線-X\" cannot be used or renamed", | |||
| "renameErr_LANE_NAME_TAKEN": "This floor and remark already has that lane", | |||
| "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "This floor already has the same name with a different remark. Delivery tickets cannot tell them apart.", | |||
| "renameErr_LANE_NOT_FOUND": "No live lane rows were found, so nothing was written", | |||
| "renameErr_LANE_TIME_INVALID": "Departure time is not a valid time", | |||
| "renameLog_title": "Lane renames", | |||
| "renameLog_line": "{{when}} {{who}}: {{from}} → {{to}} ({{floor}})", | |||
| "departureEditAria": "Edit departure time", | |||
| "departureTooltipEditSave": "Edit departure time (saved with \"Save changes\")", | |||
| "departureTooltipNeedShops": "Add shops before setting departure time", | |||
| @@ -212,9 +230,14 @@ | |||
| "err_save": "Save failed", | |||
| "exportRoutes": "Export routes", | |||
| "filter_apply": "OK", | |||
| "filter_clear": "Clear", | |||
| "filter_clear": "Clear all", | |||
| "floor_all": "All", | |||
| "floor_label": "Floor", | |||
| "lane_filter": "Filter", | |||
| "laneFilter_special": "Lane code", | |||
| "laneFilter_zone": "Zone", | |||
| "laneFilter_weekday": "Weekday", | |||
| "laneFilter_area": "Area", | |||
| "id": "id", | |||
| "importRoutes": "Import routes", | |||
| "import_staged_preview": "Import preview loaded: {{file}} ({{sheets}} sheet(s) / {{rows}} rows). Press \"Save changes\" to persist.", | |||
| @@ -34,6 +34,8 @@ | |||
| "BOM Type": "BOM 類型", | |||
| "Back": "返回", | |||
| "Back to List": "返回列表", | |||
| "Back to Process": "返回列表", | |||
| "Back to Process Steps": "返回列表", | |||
| "Bad Item Qty": "不良物品數量", | |||
| "Bad Package Qty": "不良包裝數量", | |||
| "Bag": "包裝袋", | |||
| @@ -10,6 +10,8 @@ | |||
| "Auto-refresh every 10 minutes": "每10分鐘自動刷新", | |||
| "Back": "返回", | |||
| "Back to List": "返回列表", | |||
| "Back to Process": "返回列表", | |||
| "Back to Process Steps": "返回列表", | |||
| "Bag": "包裝袋", | |||
| "Bag Consumption": "包裝袋消耗", | |||
| "Balance": "可用數量", | |||
| @@ -106,7 +106,7 @@ | |||
| "lane_noMatchFilter": "無符合條件的車線(清除搜索或樓層篩選)", | |||
| "floor_label": "樓層", | |||
| "floor_all": "全部", | |||
| "filter_clear": "清除", | |||
| "filter_clear": "清除全部", | |||
| "filter_apply": "確定", | |||
| "btn_addLane": "新增車線", | |||
| "tools_title": "操作工具", | |||
| @@ -106,7 +106,12 @@ | |||
| "lane_selectAll": "全選", | |||
| "floor_label": "樓層", | |||
| "floor_all": "全部", | |||
| "filter_clear": "清除", | |||
| "lane_filter": "篩選", | |||
| "laneFilter_special": "車線", | |||
| "laneFilter_zone": "區", | |||
| "laneFilter_weekday": "星期", | |||
| "laneFilter_area": "地區", | |||
| "filter_clear": "清除全部", | |||
| "filter_apply": "確定", | |||
| "btn_addLane": "新增車線", | |||
| "tools_title": "操作工具", | |||
| @@ -200,8 +205,26 @@ | |||
| "dialog_editLogisticsTitle": "編輯物流公司", | |||
| "btn_apply": "套用", | |||
| "addShop_confirm": "確認", | |||
| "departureDialog_title": "編輯出車時間", | |||
| "departureDialog_hint": "套用至此車線所有店鋪列。", | |||
| "departureDialog_title": "編輯車線", | |||
| "departureDialog_nameLabel": "車線名稱", | |||
| "departureDialog_hint": "名稱與出車時間會套用到此車線全部店鋪。\n 既有送貨訂單維持舊名稱;之後放單才用新名稱。\n 按上方「儲存更改」才寫入。", | |||
| "laneEditTooltip": "編輯車線名稱與出車時間(按「儲存更改」才寫入)", | |||
| "confirm_weekdayToken": "車線名稱裡的星期代碼會改變,或改完後無法辨認。4F 派車是用名稱裡的 Mon–Sun 對日子。仍要套用?", | |||
| "confirm_renameLane": "把「{{from}}」改成「{{to}}」。\n未完成出倉票 {{unfinished}} 張維持舊名稱(負責人:{{handlers}})。\n已完成 {{completed}} 張維持舊名稱。\n之後放單才用新名稱。\n未完成排程 {{schedules}} 筆的起迄名稱會改。\n按「儲存更改」後才寫入。", | |||
| "confirm_restoreKeepsTickets": "還原會把車線主檔,以及指向目前名稱的未完成排程,改回此版本的名稱。未完成的出倉票會留在現在的名稱,不會跟回去。", | |||
| "rename_noHandlers": "無", | |||
| "rename_none": "無", | |||
| "rename_savePartial": "已改名:{{ok}}。失敗:{{fail}}。失敗的那一桶沒有寫入;已經成功的不會自動改回去。請重新整理後再存。", | |||
| "renameErr_preview": "讀不到出倉票與排程數量,這次沒有套用。", | |||
| "renameErr_LANE_NAME_EMPTY": "車線名稱不可空白", | |||
| "renameErr_LANE_NAME_TOO_LONG": "車線名稱最多 50 字", | |||
| "renameErr_LANE_NAME_RESERVED": "不能使用或改掉「車線-X」", | |||
| "renameErr_LANE_NAME_TAKEN": "同一樓層、同一備註已有這條車線", | |||
| "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "同一樓層已有相同名稱但備註不同,出倉票無法分辨,已拒絕", | |||
| "renameErr_LANE_NOT_FOUND": "找不到這條車線的主檔,沒有寫入", | |||
| "renameErr_LANE_TIME_INVALID": "出車時間格式不正確", | |||
| "renameLog_title": "車線改名紀錄", | |||
| "renameLog_line": "{{when}} {{who}}:{{from}} → {{to}}({{floor}})", | |||
| "seqDialog_title": "編輯裝車順序", | |||
| "logistics_colLaneCount": "{{count}} 條車線", | |||
| "tooltip_openLaneBoard": "在車線看板開此車線", | |||