2. 新報表:送貨訂單與倉存單位不符 3 店鋪補貨報表 加「原單提料人」「實際補貨提料人」;Excel 欄位改純中文表頭 4 包裝 autoPass 5. JO 提料 re-suggest JO 類型 storeId 保持 null(避免被預設成 2/F 濾掉 3F) 前端對齊 DO:用後端 stockout qty、同 POL 依 status 排序 6. 使用者建立/驗證 7. BOM 設備 8「不合用/不適用」統一不掛 equipment data 9 hilde some useless console logfix負數倉
| @@ -42,9 +42,11 @@ const en = { | |||
| "delivery_store": "Store", | |||
| "delivery_staff": "Staff", | |||
| "delivery_staffPlaceholder": "Leave empty for all", | |||
| "delivery_staffPerfCaption": "Per-person pick count & total duration for period", | |||
| "delivery_staffPerfCaption": "Per-person pick count, item kinds, picked qty & total duration for period", | |||
| "delivery_colStaff": "Staff", | |||
| "delivery_colPickCount": "Pick Count", | |||
| "delivery_colItemKindCount": "Item Kind Count", | |||
| "delivery_colItemQtyPicked": "Item Qty Picked", | |||
| "delivery_colTotalMin": "Total Min", | |||
| "delivery_colAvgMin": "Avg Min/Order", | |||
| "delivery_dailyByStaff": "Daily by Staff", | |||
| @@ -167,9 +169,11 @@ const zh = { | |||
| "delivery_store": "倉別", | |||
| "delivery_staff": "員工", | |||
| "delivery_staffPlaceholder": "不選則全部", | |||
| "delivery_staffPerfCaption": "週期內每人揀單數及總耗時(首揀至完成)", | |||
| "delivery_staffPerfCaption": "週期內每人揀單數、品項數、揀貨數量及總耗時(首揀至完成)", | |||
| "delivery_colStaff": "員工", | |||
| "delivery_colPickCount": "揀單數", | |||
| "delivery_colItemKindCount": "品項數", | |||
| "delivery_colItemQtyPicked": "揀貨數量", | |||
| "delivery_colTotalMin": "總分鐘", | |||
| "delivery_colAvgMin": "平均分鐘/單", | |||
| "delivery_dailyByStaff": "每日按員工單數", | |||
| @@ -66,6 +66,7 @@ const defaultCriteria: Criteria = { | |||
| }, | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */ | |||
| export default function DeliveryChartPage() { | |||
| const [criteria, setCriteria] = useState<Criteria>(defaultCriteria); | |||
| const [topItemsSelected, setTopItemsSelected] = useState<TopDeliveryItemOption[]>([]); | |||
| @@ -76,7 +77,14 @@ export default function DeliveryChartPage() { | |||
| const [chartData, setChartData] = useState<{ | |||
| delivery: { date: string; orderCount: number; totalQty: number }[]; | |||
| topItems: { itemCode: string; itemName: string; totalQty: number }[]; | |||
| staffPerf: { date: string; staffName: string; orderCount: number; totalMinutes: number }[]; | |||
| staffPerf: { | |||
| date: string; | |||
| staffName: string; | |||
| orderCount: number; | |||
| totalMinutes: number; | |||
| itemKindCount: number; | |||
| itemQtyPicked: number; | |||
| }[]; | |||
| }>({ delivery: [], topItems: [], staffPerf: [] }); | |||
| const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({}); | |||
| @@ -146,6 +154,8 @@ export default function DeliveryChartPage() { | |||
| staffName: string; | |||
| orderCount: number; | |||
| totalMinutes: number; | |||
| itemKindCount: number; | |||
| itemQtyPicked: number; | |||
| }[], | |||
| })) | |||
| ) | |||
| @@ -164,18 +174,30 @@ export default function DeliveryChartPage() { | |||
| }, [criteria.topItems.rangeDays]); | |||
| const staffPerfByStaff = useMemo(() => { | |||
| const map = new Map<string, { orderCount: number; totalMinutes: number }>(); | |||
| const map = new Map< | |||
| string, | |||
| { orderCount: number; totalMinutes: number; itemKindCount: number; itemQtyPicked: number } | |||
| >(); | |||
| for (const r of chartData.staffPerf) { | |||
| const name = r.staffName || "Unknown"; | |||
| const cur = map.get(name) ?? { orderCount: 0, totalMinutes: 0 }; | |||
| const cur = map.get(name) ?? { | |||
| orderCount: 0, | |||
| totalMinutes: 0, | |||
| itemKindCount: 0, | |||
| itemQtyPicked: 0, | |||
| }; | |||
| map.set(name, { | |||
| orderCount: cur.orderCount + r.orderCount, | |||
| totalMinutes: cur.totalMinutes + r.totalMinutes, | |||
| itemKindCount: cur.itemKindCount + r.itemKindCount, | |||
| itemQtyPicked: cur.itemQtyPicked + r.itemQtyPicked, | |||
| }); | |||
| } | |||
| return Array.from(map.entries()).map(([staffName, v]) => ({ | |||
| staffName, | |||
| orderCount: v.orderCount, | |||
| itemKindCount: v.itemKindCount, | |||
| itemQtyPicked: v.itemQtyPicked, | |||
| totalMinutes: v.totalMinutes, | |||
| avgMinutesPerOrder: v.orderCount > 0 ? Math.round(v.totalMinutes / v.orderCount) : 0, | |||
| })); | |||
| @@ -298,7 +320,14 @@ export default function DeliveryChartPage() { | |||
| <ChartCard | |||
| title="員工發貨績效(每日揀貨數量與耗時)" | |||
| exportFilename="員工發貨績效" | |||
| exportData={chartData.staffPerf.map((r) => ({ 日期: r.date, 員工: r.staffName, 揀單數: r.orderCount, 總分鐘: r.totalMinutes }))} | |||
| exportData={chartData.staffPerf.map((r) => ({ | |||
| 日期: r.date, | |||
| 員工: r.staffName, | |||
| 揀單數: r.orderCount, | |||
| 總揀貨款數: r.itemKindCount, | |||
| 總揀貨件數: r.itemQtyPicked, | |||
| 總分鐘: r.totalMinutes, | |||
| }))} | |||
| filters={ | |||
| <> | |||
| <DateRangeSelect | |||
| @@ -388,7 +417,7 @@ export default function DeliveryChartPage() { | |||
| <> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Typography variant="subtitle2" color="text.secondary" gutterBottom> | |||
| 週期內每人揀單數及總耗時(首揀至完成) | |||
| 週期內每人揀單數、總揀貨款數、總揀貨件數及總耗時(首揀至完成) | |||
| </Typography> | |||
| <Box | |||
| component="table" | |||
| @@ -409,6 +438,8 @@ export default function DeliveryChartPage() { | |||
| <tr> | |||
| <th>員工</th> | |||
| <th>揀單數</th> | |||
| <th>總揀貨款數</th> | |||
| <th>總揀貨件數</th> | |||
| <th>總分鐘</th> | |||
| <th>平均分鐘/單</th> | |||
| </tr> | |||
| @@ -416,13 +447,15 @@ export default function DeliveryChartPage() { | |||
| <tbody> | |||
| {staffPerfByStaff.length === 0 ? ( | |||
| <tr> | |||
| <td colSpan={4}>無數據</td> | |||
| <td colSpan={6}>無數據</td> | |||
| </tr> | |||
| ) : ( | |||
| staffPerfByStaff.map((row) => ( | |||
| <tr key={row.staffName}> | |||
| <td>{row.staffName}</td> | |||
| <td>{row.orderCount}</td> | |||
| <td>{row.itemKindCount}</td> | |||
| <td>{row.itemQtyPicked}</td> | |||
| <td>{row.totalMinutes}</td> | |||
| <td>{row.avgMinutesPerOrder}</td> | |||
| </tr> | |||
| @@ -30,6 +30,9 @@ const REPORT_ICON_MAP: Record<string, SvgIconComponent> = { | |||
| "rep-008": OutboundOutlinedIcon, | |||
| "rep-009": OutboundOutlinedIcon, | |||
| "rep-013": LocalShippingOutlinedIcon, | |||
| "rep-016": OutboundOutlinedIcon, | |||
| "rep-017": LocalShippingOutlinedIcon, | |||
| "rep-018": SearchOutlinedIcon, | |||
| "rep-006": BarChartOutlinedIcon, | |||
| "rep-005": PieChartOutlineOutlinedIcon, | |||
| "rep-015": LayersOutlinedIcon, | |||
| @@ -44,6 +44,7 @@ interface ItemCodeWithName { | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.0 | 2026-08-10 */ | |||
| export default function ReportPage() { | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const includeGrnFinancialColumns = | |||
| @@ -213,6 +214,22 @@ export default function ReportPage() { | |||
| return false; | |||
| } | |||
| // Date fields with minDate: 'today' must not be before local today | |||
| const today = new Date(); | |||
| const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; | |||
| const beforeToday = currentReport.fields | |||
| .filter((field) => field.type === 'date' && field.minDate === 'today') | |||
| .filter((field) => { | |||
| const v = (criteria[field.name] || '').trim(); | |||
| return v && v < todayStr; | |||
| }) | |||
| .map((field) => field.label); | |||
| if (beforeToday.length > 0) { | |||
| alert(`日期不可早於今天:\n- ${beforeToday.join('\n- ')}`); | |||
| return false; | |||
| } | |||
| return true; | |||
| }; | |||
| @@ -421,6 +438,11 @@ export default function ReportPage() { | |||
| // Use larger grid size for 成品/半成品生產分析報告 | |||
| const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 }; | |||
| const todayLocal = (() => { | |||
| const d = new Date(); | |||
| return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; | |||
| })(); | |||
| const disabledByCheckedCheckbox = currentReport.fields.some((f) => { | |||
| if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false; | |||
| return f.disablesFieldsWhenChecked?.includes(field.name) ?? false; | |||
| @@ -546,6 +568,11 @@ export default function ReportPage() { | |||
| placeholder={field.placeholder} | |||
| disabled={disabledByCheckedCheckbox || disabledRep012Status} | |||
| InputLabelProps={field.type === 'date' ? { shrink: true } : {}} | |||
| inputProps={ | |||
| field.type === 'date' && field.minDate === 'today' | |||
| ? { min: todayLocal } | |||
| : undefined | |||
| } | |||
| sx={currentReport.id === 'rep-005' ? { | |||
| '& .MuiOutlinedInput-root': { | |||
| minHeight: '64px', | |||
| @@ -25,7 +25,7 @@ export const REPORT_CATEGORIES: ReportCategoryConfig[] = [ | |||
| headerBg: "#b3d4f0", | |||
| bodyBg: "#eef5fc", | |||
| accent: "#1565c0", | |||
| reportIds: ["rep-004", "rep-014", "rep-008", "rep-009", "rep-013", "rep-016", "rep-017"], | |||
| reportIds: ["rep-004", "rep-014", "rep-008", "rep-009", "rep-013", "rep-016", "rep-017", "rep-018"], | |||
| }, | |||
| { | |||
| id: "production", | |||
| @@ -13,10 +13,12 @@ export interface ShopOrderReplenishmentReportRow { | |||
| itemName?: string; | |||
| firstOrderQty?: number | string; | |||
| firstOrderActualPickQty?: number | string; | |||
| firstOrderPickerHandler?: string; | |||
| reorderQty?: number | string; | |||
| reorderDate?: string; | |||
| reason?: string; | |||
| actualDeliveredQty?: number | string; | |||
| actualDeliveredHandler?: string; | |||
| deliveredDate?: string; | |||
| [key: string]: unknown; | |||
| } | |||
| @@ -27,24 +29,25 @@ export interface ShopOrderReplenishmentReportResponse { | |||
| const SHEET_NAME = "店鋪訂單補貨記錄"; | |||
| const NO_DATA_NOTE = | |||
| "(篩選範圍內無資料 / No records in the selected range)"; | |||
| const NO_DATA_NOTE = "(篩選範圍內無資料)"; | |||
| function emptySheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> { | |||
| return { | |||
| "Shop No. / 店鋪編號": note, | |||
| "Shop Name / 店鋪名稱": "", | |||
| "Shop Order Date / 店鋪訂單日期": "", | |||
| "Shop Order No. / 店鋪訂單編號": "", | |||
| "Item No. / 貨品編號": "", | |||
| "Item Name / 貨品名稱": "", | |||
| "First Order Qty / 原訂單數量": "", | |||
| "First Order Actual Pick Qty / 原單實際提料數量": "", | |||
| "Reorder Qty / 補貨數量": "", | |||
| "Reorder Date / 補貨日期": "", | |||
| "Reason / 補貨原因": "", | |||
| "Actual Delivered Qty / 實際補貨數量": "", | |||
| "Delivered Date / 送貨日期": "", | |||
| "店鋪編號": note, | |||
| "店鋪名稱": "", | |||
| "店鋪訂單日期": "", | |||
| "店鋪訂單編號": "", | |||
| "貨品編號": "", | |||
| "貨品名稱": "", | |||
| "原訂單數量": "", | |||
| "原單實際提料數量": "", | |||
| "原單提料人": "", | |||
| "補貨數量": "", | |||
| "補貨日期": "", | |||
| "補貨原因": "", | |||
| "實際補貨數量": "", | |||
| "實際補貨提料人": "", | |||
| "送貨日期": "", | |||
| }; | |||
| } | |||
| @@ -92,19 +95,21 @@ function toExcelRow(r: ShopOrderReplenishmentReportRow): Record<string, unknown> | |||
| const base = emptySheetRow(""); | |||
| return { | |||
| ...base, | |||
| "Shop No. / 店鋪編號": r.shopNo ?? "", | |||
| "Shop Name / 店鋪名稱": r.shopName ?? "", | |||
| "Shop Order Date / 店鋪訂單日期": formatDateCell(r.shopOrderDate), | |||
| "Shop Order No. / 店鋪訂單編號": r.shopOrderNo ?? "", | |||
| "Item No. / 貨品編號": r.itemNo ?? "", | |||
| "Item Name / 貨品名稱": r.itemName ?? "", | |||
| "First Order Qty / 原訂單數量": formatQty(r.firstOrderQty), | |||
| "First Order Actual Pick Qty / 原單實際提料數量": formatQty(r.firstOrderActualPickQty), | |||
| "Reorder Qty / 補貨數量": formatQty(r.reorderQty), | |||
| "Reorder Date / 補貨日期": formatDateCell(r.reorderDate), | |||
| "Reason / 補貨原因": formatReason(r.reason), | |||
| "Actual Delivered Qty / 實際補貨數量": formatQty(r.actualDeliveredQty), | |||
| "Delivered Date / 送貨日期": formatDateCell(r.deliveredDate), | |||
| "店鋪編號": r.shopNo ?? "", | |||
| "店鋪名稱": r.shopName ?? "", | |||
| "店鋪訂單日期": formatDateCell(r.shopOrderDate), | |||
| "店鋪訂單編號": r.shopOrderNo ?? "", | |||
| "貨品編號": r.itemNo ?? "", | |||
| "貨品名稱": r.itemName ?? "", | |||
| "原訂單數量": formatQty(r.firstOrderQty), | |||
| "原單實際提料數量": formatQty(r.firstOrderActualPickQty), | |||
| "原單提料人": r.firstOrderPickerHandler ?? "", | |||
| "補貨數量": formatQty(r.reorderQty), | |||
| "補貨日期": formatDateCell(r.reorderDate), | |||
| "補貨原因": formatReason(r.reason), | |||
| "實際補貨數量": formatQty(r.actualDeliveredQty), | |||
| "實際補貨提料人": r.actualDeliveredHandler ?? "", | |||
| "送貨日期": formatDateCell(r.deliveredDate), | |||
| }; | |||
| } | |||
| @@ -132,6 +137,7 @@ export async function fetchShopOrderReplenishmentReportData( | |||
| } | |||
| /** | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 49 | v1.0.1 | 2026-08-10 | |||
| * Generate and download Shop Orders Replenishment Records as Excel. | |||
| */ | |||
| export async function generateShopOrderReplenishmentReportExcel( | |||
| @@ -131,6 +131,8 @@ export interface StaffDeliveryPerformanceRow { | |||
| staffName: string; | |||
| orderCount: number; | |||
| totalMinutes: number; | |||
| itemKindCount: number; | |||
| itemQtyPicked: number; | |||
| } | |||
| export interface StaffOption { | |||
| @@ -577,6 +579,7 @@ export async function fetchPlannedOutputByDateAndItem( | |||
| /** Warehouse / lane filter for staff delivery performance chart (delivery_order_pick_order.store_id). */ | |||
| export type StaffDeliveryPerformanceStoreFilter = "all" | "2/F" | "4/F" | "null_only"; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */ | |||
| export async function fetchStaffDeliveryPerformance( | |||
| startDate?: string, | |||
| endDate?: string, | |||
| @@ -604,6 +607,8 @@ export async function fetchStaffDeliveryPerformance( | |||
| staffName: String(row.staffName ?? row.staffname ?? ""), | |||
| orderCount: Number(row.orderCount ?? row.ordercount ?? 0), | |||
| totalMinutes: Number(row.totalMinutes ?? row.totalminutes ?? 0), | |||
| itemKindCount: Number(row.itemKindCount ?? row.itemkindcount ?? 0), | |||
| itemQtyPicked: Number(row.itemQtyPicked ?? row.itemqtypicked ?? 0), | |||
| }; | |||
| }); | |||
| } | |||
| @@ -164,6 +164,15 @@ export const updateUser = async ( | |||
| if (response.status === 401) { | |||
| throw new Error("Unauthorized: Please log in again"); | |||
| } | |||
| throw new Error(`Failed to update user: ${response.status} ${response.statusText}`); | |||
| let detail = ""; | |||
| try { | |||
| const body = await response.json(); | |||
| detail = body?.error || body?.message || ""; | |||
| } catch { | |||
| // ignore parse errors | |||
| } | |||
| throw new Error( | |||
| `Failed to update user: ${response.status} ${response.statusText}${detail ? `. ${detail}` : ""}`, | |||
| ); | |||
| } | |||
| }; | |||
| @@ -29,7 +29,7 @@ import { | |||
| useForm, | |||
| useFormContext, | |||
| } from "react-hook-form"; | |||
| import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; | |||
| import { Check, Close, Error as ErrorIcon, RestartAlt } from "@mui/icons-material"; | |||
| import { | |||
| UserInputs, | |||
| adminChangePassword, | |||
| @@ -46,8 +46,9 @@ interface Props { | |||
| auths: auth[]; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 62 | v1.0.0 | 2026-08-10 */ | |||
| const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||
| console.log(auths); | |||
| // console.log(auths); | |||
| const { t } = useTranslation("user"); | |||
| const formProps = useForm<UserInputs>(); | |||
| const searchParams = useSearchParams(); | |||
| @@ -172,7 +173,33 @@ const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||
| router.replace("/settings/user"); | |||
| } catch (e) { | |||
| console.log(e); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| const msg = e instanceof Error ? e.message : String(e); | |||
| if (msg.includes("USERNAME_NOT_AVAILABLE")) { | |||
| const text = t("Username is already taken"); | |||
| setServerError(text); | |||
| formProps.setError("username", { message: text, type: "validate" }); | |||
| } else if (msg.includes("NAME_NOT_AVAILABLE")) { | |||
| const text = t("Name is already taken"); | |||
| setServerError(text); | |||
| formProps.setError("name", { message: text, type: "validate" }); | |||
| } else if (msg.includes("STAFF_NO_NOT_AVAILABLE")) { | |||
| const text = t("Staff No is already taken"); | |||
| setServerError(text); | |||
| formProps.setError("staffNo", { message: text, type: "validate" }); | |||
| } else if (msg.includes("USER_WRONG_NEW_PWD")) { | |||
| setServerError(t("New password does not meet the rules")); | |||
| } else if (/\b400\b/.test(msg)) { | |||
| setServerError(t("Invalid request. Please check your input")); | |||
| } else if (/\b401\b/.test(msg) || /\b403\b/.test(msg)) { | |||
| setServerError(t("Unauthorized or no permission")); | |||
| } else if (/\b404\b/.test(msg)) { | |||
| setServerError(t("User Not Found")); | |||
| } else if (/\b500\b/.test(msg)) { | |||
| setServerError(t("Server error. Please try again later")); | |||
| } else { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| } | |||
| }, | |||
| [router], | |||
| @@ -212,7 +239,7 @@ const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||
| label={t("User Detail")} | |||
| icon={ | |||
| hasErrorsInTab(0, errors) ? ( | |||
| <Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
| <ErrorIcon sx={{ marginInlineEnd: 1 }} color="error" /> | |||
| ) : undefined | |||
| } | |||
| iconPosition="end" | |||
| @@ -35,6 +35,11 @@ const UserDetail: React.FC = () => { | |||
| required: "username required!", | |||
| })} | |||
| error={Boolean(errors.username)} | |||
| helperText={ | |||
| Boolean(errors.username) && errors.username?.message | |||
| ? t(errors.username.message) | |||
| : "" | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -57,7 +57,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| /* | |||
| const handleAutoCompleteChange = useCallback( | |||
| (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | |||
| console.log("BOM changed to:", value); | |||
| // console.log("BOM changed to:", value); | |||
| onChange(value.id); | |||
| // 重置倍数为 1 | |||
| @@ -98,7 +98,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| }, [bomCombo]); | |||
| const handleAutoCompleteChange = useCallback( | |||
| (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | |||
| console.log("BOM changed to:", value); | |||
| // console.log("BOM changed to:", value); | |||
| onChange(value.id); | |||
| if (value.outputQty != null) { | |||
| @@ -265,13 +265,13 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| pageSize: pagingController.pageSize, | |||
| }; | |||
| const response = await fetchJos(params); | |||
| console.log("newPageFetch params:", params) | |||
| console.log("newPageFetch response:", response) | |||
| // console.log("newPageFetch params:", params) | |||
| // console.log("newPageFetch response:", response) | |||
| if (response && response.records) { | |||
| console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| // console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| setTotalCount(response.total); | |||
| setFilteredJos(response.records); | |||
| console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| // console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| } else { | |||
| console.warn("newPageFetch - no response or no records"); | |||
| setFilteredJos([]); | |||
| @@ -266,13 +266,13 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||
| pageSize: pagingController.pageSize, | |||
| }; | |||
| const response = await fetchJosForWorkbench(params); | |||
| console.log("newPageFetch params:", params) | |||
| console.log("newPageFetch response:", response) | |||
| // console.log("newPageFetch params:", params) | |||
| // console.log("newPageFetch response:", response) | |||
| if (response && response.records) { | |||
| console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| // console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| setTotalCount(response.total); | |||
| setFilteredJos(response.records); | |||
| console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| // console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| } else { | |||
| console.warn("newPageFetch - no response or no records"); | |||
| setFilteredJos([]); | |||
| @@ -618,7 +618,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.0 | 2026-08-03 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 43 | v1.0.1 | 2026-08-10 */ | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | |||
| const workbenchMode = true; | |||
| const { t } = useTranslation("jo"); | |||
| @@ -896,10 +896,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| data.pickOrderLines.forEach((line) => { | |||
| // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次) | |||
| const lotIdSet = new Set<number>(); | |||
| /** 已由有批次建議分配的量(加總後與 pick_order_line.requiredQty 的差額 = 無批次列應顯示的數),對齊 DO Workbench */ | |||
| let lotsAllocatedSumForLine = 0; | |||
| // lots:按 lotId 去重并合并 requiredQty(对齐 GoodPickExecutiondetail) | |||
| // lots:按 lotId 去重并合并 requiredQty(对齐 DO Workbench / GoodPickExecutiondetail) | |||
| if (line.lots && line.lots.length > 0) { | |||
| const lotMap = new Map<number, any>(); | |||
| @@ -916,7 +914,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| }); | |||
| lotMap.forEach((lot: any) => { | |||
| lotsAllocatedSumForLine += Number(lot.requiredQty) || 0; | |||
| if (lot.lotId != null) lotIdSet.add(lot.lotId); | |||
| allLots.push({ | |||
| @@ -945,20 +942,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| }); | |||
| } | |||
| /** 工單 API 常在有揀貨後仍回傳 lots: [],缺口只在 stockouts;此時用非 noLot 的已揀量扣 POL(對齊實際剩餘) */ | |||
| const stockoutsPickedSumNonNoLot = (line.stockouts ?? []).reduce( | |||
| (acc: number, s: any) => { | |||
| if (!s || s.noLot) return acc; | |||
| return acc + (Number(s.qty) || 0); | |||
| }, | |||
| 0, | |||
| ); | |||
| const noLotRemainingBasis = | |||
| lotsAllocatedSumForLine > 0 | |||
| ? lotsAllocatedSumForLine | |||
| : stockoutsPickedSumNonNoLot; | |||
| // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环 | |||
| // 批號需求數:對齊 DO Workbench——用後端 stockout/SPL qty,不前端推 gap | |||
| if (line.stockouts && line.stockouts.length > 0) { | |||
| line.stockouts.forEach((stockout: any) => { | |||
| const hasLot = stockout.lotId != null; | |||
| @@ -970,6 +955,17 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| return; | |||
| } | |||
| const stockoutRequiredQty = Number( | |||
| stockout?.requiredQty ?? | |||
| stockout?.suggestedPickQty ?? | |||
| stockout?.suggestedPickLotQty, | |||
| ); | |||
| const effectiveStockoutRequiredQty = Number.isFinite( | |||
| stockoutRequiredQty, | |||
| ) | |||
| ? stockoutRequiredQty | |||
| : Number(line.requiredQty) || 0; | |||
| allLots.push({ | |||
| pickOrderLineId: line.id, | |||
| itemId: line.itemId, | |||
| @@ -996,19 +992,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| expiryDate: null, | |||
| location: stockout.location || null, | |||
| availableQty: stockout.availableQty ?? 0, | |||
| // 無批次列:有 SPL 時扣 suggested 合計;僅有 stockouts(lots 空)時扣已揀量(對齊 DO + workbench 僅 SOL 情境) | |||
| requiredQty: stockout.noLot | |||
| ? Math.max( | |||
| 0, | |||
| (Number(line.requiredQty) || 0) - noLotRemainingBasis, | |||
| ) | |||
| : Number(line.requiredQty) || 0, | |||
| requiredQty: effectiveStockoutRequiredQty, | |||
| actualPickQty: stockout.qty ?? 0, | |||
| processingStatus: stockout.status || "pending", | |||
| lotAvailability: stockout.noLot | |||
| ? "insufficient_stock" | |||
| : "available", | |||
| suggestedPickLotId: null, | |||
| suggestedPickLotId: stockout.suggestedPickLotId ?? null, | |||
| stockOutLineId: stockout.id || null, | |||
| stockOutLineQty: stockout.qty ?? 0, | |||
| stockOutLineStatus: stockout.status || null, | |||
| @@ -3655,6 +3645,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| const isNoLotTailRow = (lot: any) => | |||
| lot.noLot === true || lot.lotId == null || lot.lotId === undefined; | |||
| /** 同 POL 內對齊 DO Workbench:已掃/已完成在前,resuggest pending 在後 */ | |||
| const statusRank = (lot: any) => { | |||
| const st = String(lot?.stockOutLineStatus ?? "").toLowerCase(); | |||
| if ( | |||
| st === "completed" || | |||
| st === "partially_completed" || | |||
| st === "partially_complete" | |||
| ) { | |||
| return 0; | |||
| } | |||
| if (st === "checked") return 1; | |||
| if (st === "pending") return 2; | |||
| if (st === "rejected") return 3; | |||
| return 9; | |||
| }; | |||
| const sortedData = [...sourceData].sort((a, b) => { | |||
| const efA = effectiveFloorOrder(a); | |||
| const efB = effectiveFloorOrder(b); | |||
| @@ -3674,6 +3680,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| const bName = String(b.itemName || ""); | |||
| if (aName !== bName) return aName.localeCompare(bName); | |||
| const ra = statusRank(a); | |||
| const rb = statusRank(b); | |||
| if (ra !== rb) return ra - rb; | |||
| const tailA = isNoLotTailRow(a) ? 1 : 0; | |||
| const tailB = isNoLotTailRow(b) ? 1 : 0; | |||
| if (tailA !== tailB) return tailA - tailB; | |||
| @@ -66,7 +66,7 @@ interface ProductProcessDetailProps { | |||
| fromJosave?: boolean; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.0 | 2026-08-05 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 50 | v1.0.1 | 2026-08-10 */ | |||
| const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| jobOrderId, | |||
| onBack, | |||
| @@ -77,7 +77,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| const { t } = useTranslation(["productionProcess", "common"]); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | |||
| /** 「已完成」(Just Pass):僅 ADMIN */ | |||
| /** 「跳過」(Just Pass):僅 ADMIN */ | |||
| const canAdminPass = hasAbility(abilities, AUTH.ADMIN); | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| @@ -666,7 +666,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| const isPaused = statusLower === 'paused'; | |||
| const isPending = statusLower === 'pending' || status === ''; | |||
| const isPass = statusLower === 'pass'; | |||
| const isPassDisabled = isCompleted || isPass || !canAdminPass; | |||
| const isAutoPass = statusLower === 'autopass' || statusLower === 'auto pass'; | |||
| const isPassDisabled = isCompleted || isPass || isAutoPass || !canAdminPass; | |||
| return ( | |||
| <TableRow key={line.id}> | |||
| <TableCell> | |||
| @@ -773,6 +774,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| <Chip label={t("Pending")} color="default" size="small" /> | |||
| ) : isPaused ? ( | |||
| <Chip label={t("Paused")} color="warning" size="small" /> | |||
| ) : isAutoPass ? ( | |||
| <Chip label={t("Auto Pass")} color="default" size="small" /> | |||
| ) : isPass ? ( | |||
| <Chip label={t("Just Pass")} color="success" size="small" /> | |||
| ) : ( | |||
| @@ -178,7 +178,7 @@ function isWaitingQcPutAway( | |||
| return s !== "completed" && s !== "rejected"; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.8 | 2026-08-09 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.9 | 2026-08-10 */ | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| onSelectProcess, | |||
| printerCombo, | |||
| @@ -447,7 +447,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| return productionCache; | |||
| }, [listTab, productionCache, pendingQcCache, putawayedCache, offPlanRows]); | |||
| // QC ready: same JO — all lines Completed/Pass (sibling 包裝 auto-Pass on backend) + has SIL | |||
| // QC ready: same JO — all lines Completed/Pass/autoPass (sibling 包裝 autoPass on backend) + has SIL | |||
| const jobOrderQcReadyById = useMemo(() => { | |||
| const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>(); | |||
| for (const p of tabProcesses) { | |||
| @@ -459,8 +459,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||
| const result = new Map<number, boolean>(); | |||
| const isDone = (status: unknown) => { | |||
| const s = String(status ?? "").trim().toLowerCase(); | |||
| return s === "completed" || s === "pass"; | |||
| const s = String(status ?? "").trim().toLowerCase().replace(/\s+/g, ""); | |||
| return s === "completed" || s === "pass" || s === "autopass"; | |||
| }; | |||
| byJobOrder.forEach((jobOrderProcesses, jobOrderId) => { | |||
| @@ -55,6 +55,7 @@ interface ProductionProcessStepExecutionProps { | |||
| jobOrderId?: number; // ✅ 添加 | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.0 | 2026-08-10 */ | |||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | |||
| lineId, | |||
| onBack, | |||
| @@ -62,9 +63,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| allLines, // ✅ 添加 | |||
| jobOrderId, // ✅ 添加 | |||
| }) => { | |||
| const { t } = useTranslation( ["common","jo"]); | |||
| const { t } = useTranslation( ["common","jo","productionProcess"]); | |||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const isCompleted = lineDetail?.status === "Completed" || lineDetail?.status === "Pass"; | |||
| const lineStatusNorm = String(lineDetail?.status ?? "") | |||
| .trim() | |||
| .toLowerCase() | |||
| .replace(/\s+/g, ""); | |||
| const isCompleted = | |||
| lineStatusNorm === "completed" || | |||
| lineStatusNorm === "pass" || | |||
| lineStatusNorm === "autopass"; | |||
| const isPassStatus = lineStatusNorm === "pass"; | |||
| const isAutoPassStatus = lineStatusNorm === "autopass"; | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| @@ -161,8 +171,16 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| }, [lineId]); | |||
| useEffect(() => { | |||
| // Don't show time remaining if completed | |||
| if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") { | |||
| // Don't show time remaining if completed / pass / autoPass | |||
| const statusNorm = String(lineDetail?.status ?? "") | |||
| .trim() | |||
| .toLowerCase() | |||
| .replace(/\s+/g, ""); | |||
| if ( | |||
| statusNorm === "completed" || | |||
| statusNorm === "pass" || | |||
| statusNorm === "autopass" | |||
| ) { | |||
| console.log("Line is completed"); | |||
| setRemainingTime(null); | |||
| setIsOverTime(false); | |||
| @@ -553,9 +571,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| {isCompleted ? ( | |||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | |||
| <CardContent> | |||
| {lineDetail?.status === "Pass" ? ( | |||
| {isAutoPassStatus ? ( | |||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | |||
| {t("Passed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) | |||
| {t("Auto Pass")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| ) : isPassStatus ? ( | |||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | |||
| {t("Just Pass")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) | |||
| </Typography> | |||
| ) : ( | |||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | |||
| @@ -328,7 +328,29 @@ const UserExcelSheetView: React.FC<Props> = ({ users }) => { | |||
| } catch (error) { | |||
| console.error("Failed to save user authorities", error); | |||
| setAllUsers(cloneUserList(savedUsers)); | |||
| alert(t("Save failed. Please try again.", { defaultValue: "儲存失敗,請再試一次。" })); | |||
| const msg = error instanceof Error ? error.message : String(error); | |||
| let text = t("Save failed. Please try again.", { | |||
| defaultValue: "儲存失敗,請再試一次。", | |||
| }); | |||
| if (msg.includes("USERNAME_NOT_AVAILABLE")) { | |||
| text = t("Username is already taken"); | |||
| } else if (msg.includes("USER_WRONG_NEW_PWD")) { | |||
| text = t("New password does not meet the rules"); | |||
| } else if (/\b400\b/.test(msg)) { | |||
| text = t("Invalid request. Please check your input"); | |||
| } else if ( | |||
| msg.includes("Unauthorized") || | |||
| /\b401\b/.test(msg) || | |||
| /\b403\b/.test(msg) | |||
| ) { | |||
| text = t("Unauthorized or no permission"); | |||
| } else if (/\b404\b/.test(msg)) { | |||
| text = t("User Not Found"); | |||
| } else if (/\b500\b/.test(msg)) { | |||
| text = t("Server error. Please try again later"); | |||
| } | |||
| alert(text); | |||
| } finally { | |||
| setIsSaving(false); | |||
| saveInFlightRef.current = false; | |||
| @@ -17,6 +17,8 @@ export interface ReportField { | |||
| allowInput?: boolean; // Allow user to input custom values (for select types) | |||
| /** When checkbox is checked, disable these field names (by `name`) */ | |||
| disablesFieldsWhenChecked?: string[]; | |||
| /** For date fields: restrict picker so value cannot be before today */ | |||
| minDate?: 'today'; | |||
| } | |||
| export type ReportResponseType = 'pdf' | 'excel'; | |||
| @@ -383,4 +385,31 @@ export const REPORTS: ReportDefinition[] = [ | |||
| { label: "店鋪編號 Shop Code", name: "shopCode", type: "text", required: false, placeholder: "e.g. S001" }, | |||
| ], | |||
| }, | |||
| { | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.0 | 2026-08-10 */ | |||
| id: "rep-018", | |||
| title: "送貨訂單與倉存單位不符報告", | |||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-do-inventory-uom-mismatch`, | |||
| responseType: "excel", | |||
| fields: [ | |||
| { | |||
| label: "預計到貨日期 Estimated Arrival Date", | |||
| name: "deliveryDate", | |||
| type: "date", | |||
| required: true, | |||
| minDate: "today", | |||
| }, | |||
| { | |||
| label: "送貨訂單樓層 Floor", | |||
| name: "storeId", | |||
| type: "select", | |||
| required: false, | |||
| options: [ | |||
| { label: "全部 All (2F+4F)", value: "All" }, | |||
| { label: "2F", value: "2F" }, | |||
| { label: "4F", value: "4F" }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| ] | |||
| @@ -7,6 +7,8 @@ | |||
| "board_processLive": "Process Live Board", | |||
| "dateRange_lastDays": "Last {{d}} days", | |||
| "delivery_colAvgMin": "Avg Min/Order", | |||
| "delivery_colItemKindCount": "Item Kind Count", | |||
| "delivery_colItemQtyPicked": "Item Qty Picked", | |||
| "delivery_colPickCount": "Pick Count", | |||
| "delivery_colStaff": "Staff", | |||
| "delivery_colTotalMin": "Total Min", | |||
| @@ -19,7 +21,7 @@ | |||
| "delivery_ordersByDate": "Delivery Orders by Date", | |||
| "delivery_ordersByDate_export": "Delivery_Orders_By_Date", | |||
| "delivery_staff": "Staff", | |||
| "delivery_staffPerfCaption": "Per-person pick count & total duration for period", | |||
| "delivery_staffPerfCaption": "Per-person pick count, item kinds, picked qty & total duration for period", | |||
| "delivery_staffPerfDateError": "Staff performance start date cannot be later than end date", | |||
| "delivery_staffPerformanceTitle": "Staff Delivery Performance (Daily Pick Count & Duration)", | |||
| "delivery_staffPlaceholder": "Leave empty for all", | |||
| @@ -286,12 +286,13 @@ | |||
| "code.joStatus.storing": "Storing", | |||
| "code.joStatus.PARTIAL": "Partial", | |||
| "code.joStatus.partial": "Partial", | |||
| "code.productionStatus.Pass": "Pass", | |||
| "code.productionStatus.Pass": "Skip", | |||
| "code.productionStatus.Completed": "Completed", | |||
| "code.productionStatus.Pending": "Pending", | |||
| "code.productionStatus.Paused": "Paused", | |||
| "code.productionStatus.InProgress": "In progress", | |||
| "code.productionStatus.Skip": "Skip", | |||
| "code.productionStatus.autoPass": "Auto Skipped", | |||
| "continuousScanBlocked": "Finish current scan first", | |||
| "nodeJoOut": "Job order material issue", | |||
| "nodePoOut": "Purchase pick", | |||
| @@ -272,7 +272,9 @@ | |||
| "Overview": "Overview", | |||
| "Packaging": "Packaging", | |||
| "Partial quantity submitted. Please submit more or complete the order.": "Partial quantity submitted. Please submit more or complete the order.", | |||
| "Pass": "Pass", | |||
| "Pass": "Skip", | |||
| "Just Pass": "Skip", | |||
| "Auto Pass": "Auto Skipped", | |||
| "Passed Step": "Passed Step", | |||
| "Pause": "Pause", | |||
| "Pause Reason": "Pause Reason", | |||
| @@ -123,7 +123,8 @@ | |||
| "Job process detail: handler": "Handler", | |||
| "Job process detail: process name": "Process Name", | |||
| "Job process detail: time": "Time", | |||
| "Just Pass": "Just Pass", | |||
| "Just Pass": "Skip", | |||
| "Auto Pass": "Auto Skipped", | |||
| "Last updated": "Last updated", | |||
| "Lines with insufficient stock: ": "Lines with insufficient stock: ", | |||
| "Lines with sufficient stock: ": "Lines with sufficient stock: ", | |||
| @@ -46,5 +46,12 @@ | |||
| "Failed to search by name": "Failed to search by name", | |||
| "Failed to search by username": "Failed to search by username", | |||
| "Staff No is required": "Staff No is required", | |||
| "User Not Found": "User Not Found" | |||
| "User Not Found": "User Not Found", | |||
| "Username is already taken": "Username is already taken. Please choose another.", | |||
| "Name is already taken": "Name is already taken. Please choose another.", | |||
| "Staff No is already taken": "Staff No is already taken. Please choose another.", | |||
| "New password does not meet the rules": "New password does not meet the rules. Please try again.", | |||
| "Invalid request. Please check your input": "Invalid request. Please check your input.", | |||
| "Unauthorized or no permission": "Unauthorized or no permission.", | |||
| "Server error. Please try again later": "Server error. Please try again later." | |||
| } | |||
| @@ -7,6 +7,8 @@ | |||
| "board_processLive": "工序即時看板", | |||
| "dateRange_lastDays": "最近 {{d}} 天", | |||
| "delivery_colAvgMin": "平均分鐘/單", | |||
| "delivery_colItemKindCount": "總揀貨款數", | |||
| "delivery_colItemQtyPicked": "揀貨數量", | |||
| "delivery_colPickCount": "揀單數", | |||
| "delivery_colStaff": "員工", | |||
| "delivery_colTotalMin": "總分鐘", | |||
| @@ -19,7 +21,7 @@ | |||
| "delivery_ordersByDate": "按日期發貨單數量", | |||
| "delivery_ordersByDate_export": "發貨單數量_按日期", | |||
| "delivery_staff": "員工", | |||
| "delivery_staffPerfCaption": "週期內每人揀單數及總耗時(首揀至完成)", | |||
| "delivery_staffPerfCaption": "週期內每人揀單數、總揀貨款數、揀貨數量及總耗時(首揀至完成)", | |||
| "delivery_staffPerfDateError": "員工發貨績效的起始日期不能晚於結束日期", | |||
| "delivery_staffPerformanceTitle": "員工發貨績效(每日揀貨數量與耗時)", | |||
| "delivery_staffPlaceholder": "不選則全部", | |||
| @@ -286,12 +286,13 @@ | |||
| "code.joStatus.storing": "待QC上架", | |||
| "code.joStatus.PARTIAL": "部分完成", | |||
| "code.joStatus.partial": "部分完成", | |||
| "code.productionStatus.Pass": "通過", | |||
| "code.productionStatus.Pass": "跳過", | |||
| "code.productionStatus.Completed": "完成", | |||
| "code.productionStatus.Pending": "待處理", | |||
| "code.productionStatus.Paused": "已暫停", | |||
| "code.productionStatus.InProgress": "進行中", | |||
| "code.productionStatus.Skip": "跳過", | |||
| "code.productionStatus.autoPass": "已自動跳過", | |||
| "continuousScanBlocked": "請先完成目前掃描", | |||
| "nodeJoOut": "工單提料", | |||
| "nodePoOut": "採購提料", | |||
| @@ -10,7 +10,8 @@ | |||
| "Actual Pick Qty": "實際提料數量", | |||
| "Add Bag": "新增包裝袋", | |||
| "Add Record": "添加記錄", | |||
| "Just Pass": "通過", | |||
| "Just Pass": "跳過", | |||
| "Auto Pass": "已自動跳過", | |||
| "Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中", | |||
| "Add some entries!": "請添加條目", | |||
| "All": "全部", | |||
| @@ -275,7 +276,7 @@ | |||
| "Overview": "總覽", | |||
| "Packaging": "提料中", | |||
| "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", | |||
| "Pass": "通過", | |||
| "Pass": "跳過", | |||
| "Passed Step": "通過步驟", | |||
| "Pause": "暫停", | |||
| "Pause Reason": "暫停原因", | |||
| @@ -32,7 +32,7 @@ | |||
| "Confirm": "確認", | |||
| "Confirm cancel job order": "確認取消工單", | |||
| "Confirm delete job order": "確認刪除工單", | |||
| "Confirm to Pass this Process?": "確認要通過此工序嗎?", | |||
| "Confirm to Pass this Process?": "確認要跳過此工序嗎?", | |||
| "Confirm to update this Job Order?": "確認要完成此工單嗎?", | |||
| "Consumed Qty": "消耗數量", | |||
| "Continue": "繼續", | |||
| @@ -123,7 +123,8 @@ | |||
| "Job process detail: handler": "員工", | |||
| "Job process detail: process name": "工序", | |||
| "Job process detail: time": "時間", | |||
| "Just Pass": "已完成", | |||
| "Just Pass": "跳過", | |||
| "Auto Pass": "已自動跳過", | |||
| "Last updated": "最後更新", | |||
| "Lines with insufficient stock: ": "未能提料項目數量: ", | |||
| "Lines with sufficient stock: ": "可提料項目數量: ", | |||
| @@ -46,5 +46,12 @@ | |||
| "Failed to search by name": "依名稱搜尋失敗", | |||
| "Failed to search by username": "依使用者名稱搜尋失敗", | |||
| "Staff No is required": "員工編號必填", | |||
| "User Not Found": "用戶不存在" | |||
| "User Not Found": "用戶不存在", | |||
| "Username is already taken": "用戶名稱已被使用,請換一個。", | |||
| "Name is already taken": "姓名已被使用,請換一個。", | |||
| "Staff No is already taken": "員工編號已被使用,請換一個。", | |||
| "New password does not meet the rules": "新密碼不符合規則,請重新輸入。", | |||
| "Invalid request. Please check your input": "請求資料不正確,請檢查後再試。", | |||
| "Unauthorized or no permission": "未授權或沒有權限。", | |||
| "Server error. Please try again later": "伺服器錯誤,請稍後再試。" | |||
| } | |||