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_store": "Store", | ||||
| "delivery_staff": "Staff", | "delivery_staff": "Staff", | ||||
| "delivery_staffPlaceholder": "Leave empty for all", | "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_colStaff": "Staff", | ||||
| "delivery_colPickCount": "Pick Count", | "delivery_colPickCount": "Pick Count", | ||||
| "delivery_colItemKindCount": "Item Kind Count", | |||||
| "delivery_colItemQtyPicked": "Item Qty Picked", | |||||
| "delivery_colTotalMin": "Total Min", | "delivery_colTotalMin": "Total Min", | ||||
| "delivery_colAvgMin": "Avg Min/Order", | "delivery_colAvgMin": "Avg Min/Order", | ||||
| "delivery_dailyByStaff": "Daily by Staff", | "delivery_dailyByStaff": "Daily by Staff", | ||||
| @@ -167,9 +169,11 @@ const zh = { | |||||
| "delivery_store": "倉別", | "delivery_store": "倉別", | ||||
| "delivery_staff": "員工", | "delivery_staff": "員工", | ||||
| "delivery_staffPlaceholder": "不選則全部", | "delivery_staffPlaceholder": "不選則全部", | ||||
| "delivery_staffPerfCaption": "週期內每人揀單數及總耗時(首揀至完成)", | |||||
| "delivery_staffPerfCaption": "週期內每人揀單數、品項數、揀貨數量及總耗時(首揀至完成)", | |||||
| "delivery_colStaff": "員工", | "delivery_colStaff": "員工", | ||||
| "delivery_colPickCount": "揀單數", | "delivery_colPickCount": "揀單數", | ||||
| "delivery_colItemKindCount": "品項數", | |||||
| "delivery_colItemQtyPicked": "揀貨數量", | |||||
| "delivery_colTotalMin": "總分鐘", | "delivery_colTotalMin": "總分鐘", | ||||
| "delivery_colAvgMin": "平均分鐘/單", | "delivery_colAvgMin": "平均分鐘/單", | ||||
| "delivery_dailyByStaff": "每日按員工單數", | "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() { | export default function DeliveryChartPage() { | ||||
| const [criteria, setCriteria] = useState<Criteria>(defaultCriteria); | const [criteria, setCriteria] = useState<Criteria>(defaultCriteria); | ||||
| const [topItemsSelected, setTopItemsSelected] = useState<TopDeliveryItemOption[]>([]); | const [topItemsSelected, setTopItemsSelected] = useState<TopDeliveryItemOption[]>([]); | ||||
| @@ -76,7 +77,14 @@ export default function DeliveryChartPage() { | |||||
| const [chartData, setChartData] = useState<{ | const [chartData, setChartData] = useState<{ | ||||
| delivery: { date: string; orderCount: number; totalQty: number }[]; | delivery: { date: string; orderCount: number; totalQty: number }[]; | ||||
| topItems: { itemCode: string; itemName: string; 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: [] }); | }>({ delivery: [], topItems: [], staffPerf: [] }); | ||||
| const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({}); | const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({}); | ||||
| @@ -146,6 +154,8 @@ export default function DeliveryChartPage() { | |||||
| staffName: string; | staffName: string; | ||||
| orderCount: number; | orderCount: number; | ||||
| totalMinutes: number; | totalMinutes: number; | ||||
| itemKindCount: number; | |||||
| itemQtyPicked: number; | |||||
| }[], | }[], | ||||
| })) | })) | ||||
| ) | ) | ||||
| @@ -164,18 +174,30 @@ export default function DeliveryChartPage() { | |||||
| }, [criteria.topItems.rangeDays]); | }, [criteria.topItems.rangeDays]); | ||||
| const staffPerfByStaff = useMemo(() => { | 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) { | for (const r of chartData.staffPerf) { | ||||
| const name = r.staffName || "Unknown"; | 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, { | map.set(name, { | ||||
| orderCount: cur.orderCount + r.orderCount, | orderCount: cur.orderCount + r.orderCount, | ||||
| totalMinutes: cur.totalMinutes + r.totalMinutes, | totalMinutes: cur.totalMinutes + r.totalMinutes, | ||||
| itemKindCount: cur.itemKindCount + r.itemKindCount, | |||||
| itemQtyPicked: cur.itemQtyPicked + r.itemQtyPicked, | |||||
| }); | }); | ||||
| } | } | ||||
| return Array.from(map.entries()).map(([staffName, v]) => ({ | return Array.from(map.entries()).map(([staffName, v]) => ({ | ||||
| staffName, | staffName, | ||||
| orderCount: v.orderCount, | orderCount: v.orderCount, | ||||
| itemKindCount: v.itemKindCount, | |||||
| itemQtyPicked: v.itemQtyPicked, | |||||
| totalMinutes: v.totalMinutes, | totalMinutes: v.totalMinutes, | ||||
| avgMinutesPerOrder: v.orderCount > 0 ? Math.round(v.totalMinutes / v.orderCount) : 0, | avgMinutesPerOrder: v.orderCount > 0 ? Math.round(v.totalMinutes / v.orderCount) : 0, | ||||
| })); | })); | ||||
| @@ -298,7 +320,14 @@ export default function DeliveryChartPage() { | |||||
| <ChartCard | <ChartCard | ||||
| title="員工發貨績效(每日揀貨數量與耗時)" | title="員工發貨績效(每日揀貨數量與耗時)" | ||||
| exportFilename="員工發貨績效" | 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={ | filters={ | ||||
| <> | <> | ||||
| <DateRangeSelect | <DateRangeSelect | ||||
| @@ -388,7 +417,7 @@ export default function DeliveryChartPage() { | |||||
| <> | <> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| <Typography variant="subtitle2" color="text.secondary" gutterBottom> | <Typography variant="subtitle2" color="text.secondary" gutterBottom> | ||||
| 週期內每人揀單數及總耗時(首揀至完成) | |||||
| 週期內每人揀單數、總揀貨款數、總揀貨件數及總耗時(首揀至完成) | |||||
| </Typography> | </Typography> | ||||
| <Box | <Box | ||||
| component="table" | component="table" | ||||
| @@ -409,6 +438,8 @@ export default function DeliveryChartPage() { | |||||
| <tr> | <tr> | ||||
| <th>員工</th> | <th>員工</th> | ||||
| <th>揀單數</th> | <th>揀單數</th> | ||||
| <th>總揀貨款數</th> | |||||
| <th>總揀貨件數</th> | |||||
| <th>總分鐘</th> | <th>總分鐘</th> | ||||
| <th>平均分鐘/單</th> | <th>平均分鐘/單</th> | ||||
| </tr> | </tr> | ||||
| @@ -416,13 +447,15 @@ export default function DeliveryChartPage() { | |||||
| <tbody> | <tbody> | ||||
| {staffPerfByStaff.length === 0 ? ( | {staffPerfByStaff.length === 0 ? ( | ||||
| <tr> | <tr> | ||||
| <td colSpan={4}>無數據</td> | |||||
| <td colSpan={6}>無數據</td> | |||||
| </tr> | </tr> | ||||
| ) : ( | ) : ( | ||||
| staffPerfByStaff.map((row) => ( | staffPerfByStaff.map((row) => ( | ||||
| <tr key={row.staffName}> | <tr key={row.staffName}> | ||||
| <td>{row.staffName}</td> | <td>{row.staffName}</td> | ||||
| <td>{row.orderCount}</td> | <td>{row.orderCount}</td> | ||||
| <td>{row.itemKindCount}</td> | |||||
| <td>{row.itemQtyPicked}</td> | |||||
| <td>{row.totalMinutes}</td> | <td>{row.totalMinutes}</td> | ||||
| <td>{row.avgMinutesPerOrder}</td> | <td>{row.avgMinutesPerOrder}</td> | ||||
| </tr> | </tr> | ||||
| @@ -30,6 +30,9 @@ const REPORT_ICON_MAP: Record<string, SvgIconComponent> = { | |||||
| "rep-008": OutboundOutlinedIcon, | "rep-008": OutboundOutlinedIcon, | ||||
| "rep-009": OutboundOutlinedIcon, | "rep-009": OutboundOutlinedIcon, | ||||
| "rep-013": LocalShippingOutlinedIcon, | "rep-013": LocalShippingOutlinedIcon, | ||||
| "rep-016": OutboundOutlinedIcon, | |||||
| "rep-017": LocalShippingOutlinedIcon, | |||||
| "rep-018": SearchOutlinedIcon, | |||||
| "rep-006": BarChartOutlinedIcon, | "rep-006": BarChartOutlinedIcon, | ||||
| "rep-005": PieChartOutlineOutlinedIcon, | "rep-005": PieChartOutlineOutlinedIcon, | ||||
| "rep-015": LayersOutlinedIcon, | "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. 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() { | export default function ReportPage() { | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const includeGrnFinancialColumns = | const includeGrnFinancialColumns = | ||||
| @@ -213,6 +214,22 @@ export default function ReportPage() { | |||||
| return false; | 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; | return true; | ||||
| }; | }; | ||||
| @@ -421,6 +438,11 @@ export default function ReportPage() { | |||||
| // Use larger grid size for 成品/半成品生產分析報告 | // Use larger grid size for 成品/半成品生產分析報告 | ||||
| const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 }; | 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) => { | const disabledByCheckedCheckbox = currentReport.fields.some((f) => { | ||||
| if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false; | if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false; | ||||
| return f.disablesFieldsWhenChecked?.includes(field.name) ?? false; | return f.disablesFieldsWhenChecked?.includes(field.name) ?? false; | ||||
| @@ -546,6 +568,11 @@ export default function ReportPage() { | |||||
| placeholder={field.placeholder} | placeholder={field.placeholder} | ||||
| disabled={disabledByCheckedCheckbox || disabledRep012Status} | disabled={disabledByCheckedCheckbox || disabledRep012Status} | ||||
| InputLabelProps={field.type === 'date' ? { shrink: true } : {}} | InputLabelProps={field.type === 'date' ? { shrink: true } : {}} | ||||
| inputProps={ | |||||
| field.type === 'date' && field.minDate === 'today' | |||||
| ? { min: todayLocal } | |||||
| : undefined | |||||
| } | |||||
| sx={currentReport.id === 'rep-005' ? { | sx={currentReport.id === 'rep-005' ? { | ||||
| '& .MuiOutlinedInput-root': { | '& .MuiOutlinedInput-root': { | ||||
| minHeight: '64px', | minHeight: '64px', | ||||
| @@ -25,7 +25,7 @@ export const REPORT_CATEGORIES: ReportCategoryConfig[] = [ | |||||
| headerBg: "#b3d4f0", | headerBg: "#b3d4f0", | ||||
| bodyBg: "#eef5fc", | bodyBg: "#eef5fc", | ||||
| accent: "#1565c0", | 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", | id: "production", | ||||
| @@ -13,10 +13,12 @@ export interface ShopOrderReplenishmentReportRow { | |||||
| itemName?: string; | itemName?: string; | ||||
| firstOrderQty?: number | string; | firstOrderQty?: number | string; | ||||
| firstOrderActualPickQty?: number | string; | firstOrderActualPickQty?: number | string; | ||||
| firstOrderPickerHandler?: string; | |||||
| reorderQty?: number | string; | reorderQty?: number | string; | ||||
| reorderDate?: string; | reorderDate?: string; | ||||
| reason?: string; | reason?: string; | ||||
| actualDeliveredQty?: number | string; | actualDeliveredQty?: number | string; | ||||
| actualDeliveredHandler?: string; | |||||
| deliveredDate?: string; | deliveredDate?: string; | ||||
| [key: string]: unknown; | [key: string]: unknown; | ||||
| } | } | ||||
| @@ -27,24 +29,25 @@ export interface ShopOrderReplenishmentReportResponse { | |||||
| const SHEET_NAME = "店鋪訂單補貨記錄"; | 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> { | function emptySheetRow(note: string = NO_DATA_NOTE): Record<string, unknown> { | ||||
| return { | 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(""); | const base = emptySheetRow(""); | ||||
| return { | return { | ||||
| ...base, | ...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. | * Generate and download Shop Orders Replenishment Records as Excel. | ||||
| */ | */ | ||||
| export async function generateShopOrderReplenishmentReportExcel( | export async function generateShopOrderReplenishmentReportExcel( | ||||
| @@ -131,6 +131,8 @@ export interface StaffDeliveryPerformanceRow { | |||||
| staffName: string; | staffName: string; | ||||
| orderCount: number; | orderCount: number; | ||||
| totalMinutes: number; | totalMinutes: number; | ||||
| itemKindCount: number; | |||||
| itemQtyPicked: number; | |||||
| } | } | ||||
| export interface StaffOption { | 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). */ | /** Warehouse / lane filter for staff delivery performance chart (delivery_order_pick_order.store_id). */ | ||||
| export type StaffDeliveryPerformanceStoreFilter = "all" | "2/F" | "4/F" | "null_only"; | 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( | export async function fetchStaffDeliveryPerformance( | ||||
| startDate?: string, | startDate?: string, | ||||
| endDate?: string, | endDate?: string, | ||||
| @@ -604,6 +607,8 @@ export async function fetchStaffDeliveryPerformance( | |||||
| staffName: String(row.staffName ?? row.staffname ?? ""), | staffName: String(row.staffName ?? row.staffname ?? ""), | ||||
| orderCount: Number(row.orderCount ?? row.ordercount ?? 0), | orderCount: Number(row.orderCount ?? row.ordercount ?? 0), | ||||
| totalMinutes: Number(row.totalMinutes ?? row.totalminutes ?? 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) { | if (response.status === 401) { | ||||
| throw new Error("Unauthorized: Please log in again"); | 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, | useForm, | ||||
| useFormContext, | useFormContext, | ||||
| } from "react-hook-form"; | } 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 { | import { | ||||
| UserInputs, | UserInputs, | ||||
| adminChangePassword, | adminChangePassword, | ||||
| @@ -46,8 +46,9 @@ interface Props { | |||||
| auths: auth[]; | auths: auth[]; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 62 | v1.0.0 | 2026-08-10 */ | |||||
| const CreateUser: React.FC<Props> = ({ rules, auths }) => { | const CreateUser: React.FC<Props> = ({ rules, auths }) => { | ||||
| console.log(auths); | |||||
| // console.log(auths); | |||||
| const { t } = useTranslation("user"); | const { t } = useTranslation("user"); | ||||
| const formProps = useForm<UserInputs>(); | const formProps = useForm<UserInputs>(); | ||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| @@ -172,7 +173,33 @@ const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||||
| router.replace("/settings/user"); | router.replace("/settings/user"); | ||||
| } catch (e) { | } catch (e) { | ||||
| console.log(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], | [router], | ||||
| @@ -212,7 +239,7 @@ const CreateUser: React.FC<Props> = ({ rules, auths }) => { | |||||
| label={t("User Detail")} | label={t("User Detail")} | ||||
| icon={ | icon={ | ||||
| hasErrorsInTab(0, errors) ? ( | hasErrorsInTab(0, errors) ? ( | ||||
| <Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
| <ErrorIcon sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
| ) : undefined | ) : undefined | ||||
| } | } | ||||
| iconPosition="end" | iconPosition="end" | ||||
| @@ -35,6 +35,11 @@ const UserDetail: React.FC = () => { | |||||
| required: "username required!", | required: "username required!", | ||||
| })} | })} | ||||
| error={Boolean(errors.username)} | error={Boolean(errors.username)} | ||||
| helperText={ | |||||
| Boolean(errors.username) && errors.username?.message | |||||
| ? t(errors.username.message) | |||||
| : "" | |||||
| } | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| @@ -57,7 +57,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| /* | /* | ||||
| const handleAutoCompleteChange = useCallback( | const handleAutoCompleteChange = useCallback( | ||||
| (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | (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); | onChange(value.id); | ||||
| // 重置倍数为 1 | // 重置倍数为 1 | ||||
| @@ -98,7 +98,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| }, [bomCombo]); | }, [bomCombo]); | ||||
| const handleAutoCompleteChange = useCallback( | const handleAutoCompleteChange = useCallback( | ||||
| (event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => { | (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); | onChange(value.id); | ||||
| if (value.outputQty != null) { | if (value.outputQty != null) { | ||||
| @@ -265,13 +265,13 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| }; | }; | ||||
| const response = await fetchJos(params); | 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) { | 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); | setTotalCount(response.total); | ||||
| setFilteredJos(response.records); | 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 { | } else { | ||||
| console.warn("newPageFetch - no response or no records"); | console.warn("newPageFetch - no response or no records"); | ||||
| setFilteredJos([]); | setFilteredJos([]); | ||||
| @@ -266,13 +266,13 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||||
| pageSize: pagingController.pageSize, | pageSize: pagingController.pageSize, | ||||
| }; | }; | ||||
| const response = await fetchJosForWorkbench(params); | 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) { | 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); | setTotalCount(response.total); | ||||
| setFilteredJos(response.records); | 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 { | } else { | ||||
| console.warn("newPageFetch - no response or no records"); | console.warn("newPageFetch - no response or no records"); | ||||
| setFilteredJos([]); | 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 JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | ||||
| const workbenchMode = true; | const workbenchMode = true; | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| @@ -896,10 +896,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| data.pickOrderLines.forEach((line) => { | data.pickOrderLines.forEach((line) => { | ||||
| // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次) | // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次) | ||||
| const lotIdSet = new Set<number>(); | 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) { | if (line.lots && line.lots.length > 0) { | ||||
| const lotMap = new Map<number, any>(); | const lotMap = new Map<number, any>(); | ||||
| @@ -916,7 +914,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| }); | }); | ||||
| lotMap.forEach((lot: any) => { | lotMap.forEach((lot: any) => { | ||||
| lotsAllocatedSumForLine += Number(lot.requiredQty) || 0; | |||||
| if (lot.lotId != null) lotIdSet.add(lot.lotId); | if (lot.lotId != null) lotIdSet.add(lot.lotId); | ||||
| allLots.push({ | 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 闭环 | // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环 | ||||
| // 批號需求數:對齊 DO Workbench——用後端 stockout/SPL qty,不前端推 gap | |||||
| if (line.stockouts && line.stockouts.length > 0) { | if (line.stockouts && line.stockouts.length > 0) { | ||||
| line.stockouts.forEach((stockout: any) => { | line.stockouts.forEach((stockout: any) => { | ||||
| const hasLot = stockout.lotId != null; | const hasLot = stockout.lotId != null; | ||||
| @@ -970,6 +955,17 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| return; | return; | ||||
| } | } | ||||
| const stockoutRequiredQty = Number( | |||||
| stockout?.requiredQty ?? | |||||
| stockout?.suggestedPickQty ?? | |||||
| stockout?.suggestedPickLotQty, | |||||
| ); | |||||
| const effectiveStockoutRequiredQty = Number.isFinite( | |||||
| stockoutRequiredQty, | |||||
| ) | |||||
| ? stockoutRequiredQty | |||||
| : Number(line.requiredQty) || 0; | |||||
| allLots.push({ | allLots.push({ | ||||
| pickOrderLineId: line.id, | pickOrderLineId: line.id, | ||||
| itemId: line.itemId, | itemId: line.itemId, | ||||
| @@ -996,19 +992,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| expiryDate: null, | expiryDate: null, | ||||
| location: stockout.location || null, | location: stockout.location || null, | ||||
| availableQty: stockout.availableQty ?? 0, | 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, | actualPickQty: stockout.qty ?? 0, | ||||
| processingStatus: stockout.status || "pending", | processingStatus: stockout.status || "pending", | ||||
| lotAvailability: stockout.noLot | lotAvailability: stockout.noLot | ||||
| ? "insufficient_stock" | ? "insufficient_stock" | ||||
| : "available", | : "available", | ||||
| suggestedPickLotId: null, | |||||
| suggestedPickLotId: stockout.suggestedPickLotId ?? null, | |||||
| stockOutLineId: stockout.id || null, | stockOutLineId: stockout.id || null, | ||||
| stockOutLineQty: stockout.qty ?? 0, | stockOutLineQty: stockout.qty ?? 0, | ||||
| stockOutLineStatus: stockout.status || null, | stockOutLineStatus: stockout.status || null, | ||||
| @@ -3655,6 +3645,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| const isNoLotTailRow = (lot: any) => | const isNoLotTailRow = (lot: any) => | ||||
| lot.noLot === true || lot.lotId == null || lot.lotId === undefined; | 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 sortedData = [...sourceData].sort((a, b) => { | ||||
| const efA = effectiveFloorOrder(a); | const efA = effectiveFloorOrder(a); | ||||
| const efB = effectiveFloorOrder(b); | const efB = effectiveFloorOrder(b); | ||||
| @@ -3674,6 +3680,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| const bName = String(b.itemName || ""); | const bName = String(b.itemName || ""); | ||||
| if (aName !== bName) return aName.localeCompare(bName); | 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 tailA = isNoLotTailRow(a) ? 1 : 0; | ||||
| const tailB = isNoLotTailRow(b) ? 1 : 0; | const tailB = isNoLotTailRow(b) ? 1 : 0; | ||||
| if (tailA !== tailB) return tailA - tailB; | if (tailA !== tailB) return tailA - tailB; | ||||
| @@ -66,7 +66,7 @@ interface ProductProcessDetailProps { | |||||
| fromJosave?: boolean; | 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> = ({ | const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | ||||
| jobOrderId, | jobOrderId, | ||||
| onBack, | onBack, | ||||
| @@ -77,7 +77,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| const { t } = useTranslation(["productionProcess", "common"]); | const { t } = useTranslation(["productionProcess", "common"]); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const abilities = session?.abilities ?? session?.user?.abilities ?? []; | const abilities = session?.abilities ?? session?.user?.abilities ?? []; | ||||
| /** 「已完成」(Just Pass):僅 ADMIN */ | |||||
| /** 「跳過」(Just Pass):僅 ADMIN */ | |||||
| const canAdminPass = hasAbility(abilities, AUTH.ADMIN); | const canAdminPass = hasAbility(abilities, AUTH.ADMIN); | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| @@ -666,7 +666,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| const isPaused = statusLower === 'paused'; | const isPaused = statusLower === 'paused'; | ||||
| const isPending = statusLower === 'pending' || status === ''; | const isPending = statusLower === 'pending' || status === ''; | ||||
| const isPass = statusLower === 'pass'; | const isPass = statusLower === 'pass'; | ||||
| const isPassDisabled = isCompleted || isPass || !canAdminPass; | |||||
| const isAutoPass = statusLower === 'autopass' || statusLower === 'auto pass'; | |||||
| const isPassDisabled = isCompleted || isPass || isAutoPass || !canAdminPass; | |||||
| return ( | return ( | ||||
| <TableRow key={line.id}> | <TableRow key={line.id}> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -773,6 +774,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <Chip label={t("Pending")} color="default" size="small" /> | <Chip label={t("Pending")} color="default" size="small" /> | ||||
| ) : isPaused ? ( | ) : isPaused ? ( | ||||
| <Chip label={t("Paused")} color="warning" size="small" /> | <Chip label={t("Paused")} color="warning" size="small" /> | ||||
| ) : isAutoPass ? ( | |||||
| <Chip label={t("Auto Pass")} color="default" size="small" /> | |||||
| ) : isPass ? ( | ) : isPass ? ( | ||||
| <Chip label={t("Just Pass")} color="success" size="small" /> | <Chip label={t("Just Pass")} color="success" size="small" /> | ||||
| ) : ( | ) : ( | ||||
| @@ -178,7 +178,7 @@ function isWaitingQcPutAway( | |||||
| return s !== "completed" && s !== "rejected"; | 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> = ({ | const ProductProcessList: React.FC<ProductProcessListProps> = ({ | ||||
| onSelectProcess, | onSelectProcess, | ||||
| printerCombo, | printerCombo, | ||||
| @@ -447,7 +447,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| return productionCache; | return productionCache; | ||||
| }, [listTab, productionCache, pendingQcCache, putawayedCache, offPlanRows]); | }, [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 jobOrderQcReadyById = useMemo(() => { | ||||
| const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>(); | const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>(); | ||||
| for (const p of tabProcesses) { | for (const p of tabProcesses) { | ||||
| @@ -459,8 +459,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ | |||||
| const result = new Map<number, boolean>(); | const result = new Map<number, boolean>(); | ||||
| const isDone = (status: unknown) => { | 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) => { | byJobOrder.forEach((jobOrderProcesses, jobOrderId) => { | ||||
| @@ -55,6 +55,7 @@ interface ProductionProcessStepExecutionProps { | |||||
| jobOrderId?: number; // ✅ 添加 | jobOrderId?: number; // ✅ 添加 | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 59 | v1.0.0 | 2026-08-10 */ | |||||
| const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | ||||
| lineId, | lineId, | ||||
| onBack, | onBack, | ||||
| @@ -62,9 +63,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| allLines, // ✅ 添加 | allLines, // ✅ 添加 | ||||
| jobOrderId, // ✅ 添加 | jobOrderId, // ✅ 添加 | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation( ["common","jo"]); | |||||
| const { t } = useTranslation( ["common","jo","productionProcess"]); | |||||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | 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 & { | const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | ||||
| byproductName: string; | byproductName: string; | ||||
| byproductQty: number; | byproductQty: number; | ||||
| @@ -161,8 +171,16 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| }, [lineId]); | }, [lineId]); | ||||
| useEffect(() => { | 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"); | console.log("Line is completed"); | ||||
| setRemainingTime(null); | setRemainingTime(null); | ||||
| setIsOverTime(false); | setIsOverTime(false); | ||||
| @@ -553,9 +571,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| {isCompleted ? ( | {isCompleted ? ( | ||||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | ||||
| <CardContent> | <CardContent> | ||||
| {lineDetail?.status === "Pass" ? ( | |||||
| {isAutoPassStatus ? ( | |||||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | <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> | ||||
| ) : ( | ) : ( | ||||
| <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | <Typography variant="h5" color="success.main" gutterBottom fontWeight="bold"> | ||||
| @@ -328,7 +328,29 @@ const UserExcelSheetView: React.FC<Props> = ({ users }) => { | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to save user authorities", error); | console.error("Failed to save user authorities", error); | ||||
| setAllUsers(cloneUserList(savedUsers)); | 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 { | } finally { | ||||
| setIsSaving(false); | setIsSaving(false); | ||||
| saveInFlightRef.current = false; | saveInFlightRef.current = false; | ||||
| @@ -17,6 +17,8 @@ export interface ReportField { | |||||
| allowInput?: boolean; // Allow user to input custom values (for select types) | allowInput?: boolean; // Allow user to input custom values (for select types) | ||||
| /** When checkbox is checked, disable these field names (by `name`) */ | /** When checkbox is checked, disable these field names (by `name`) */ | ||||
| disablesFieldsWhenChecked?: string[]; | disablesFieldsWhenChecked?: string[]; | ||||
| /** For date fields: restrict picker so value cannot be before today */ | |||||
| minDate?: 'today'; | |||||
| } | } | ||||
| export type ReportResponseType = 'pdf' | 'excel'; | 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" }, | { 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", | "board_processLive": "Process Live Board", | ||||
| "dateRange_lastDays": "Last {{d}} days", | "dateRange_lastDays": "Last {{d}} days", | ||||
| "delivery_colAvgMin": "Avg Min/Order", | "delivery_colAvgMin": "Avg Min/Order", | ||||
| "delivery_colItemKindCount": "Item Kind Count", | |||||
| "delivery_colItemQtyPicked": "Item Qty Picked", | |||||
| "delivery_colPickCount": "Pick Count", | "delivery_colPickCount": "Pick Count", | ||||
| "delivery_colStaff": "Staff", | "delivery_colStaff": "Staff", | ||||
| "delivery_colTotalMin": "Total Min", | "delivery_colTotalMin": "Total Min", | ||||
| @@ -19,7 +21,7 @@ | |||||
| "delivery_ordersByDate": "Delivery Orders by Date", | "delivery_ordersByDate": "Delivery Orders by Date", | ||||
| "delivery_ordersByDate_export": "Delivery_Orders_By_Date", | "delivery_ordersByDate_export": "Delivery_Orders_By_Date", | ||||
| "delivery_staff": "Staff", | "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_staffPerfDateError": "Staff performance start date cannot be later than end date", | ||||
| "delivery_staffPerformanceTitle": "Staff Delivery Performance (Daily Pick Count & Duration)", | "delivery_staffPerformanceTitle": "Staff Delivery Performance (Daily Pick Count & Duration)", | ||||
| "delivery_staffPlaceholder": "Leave empty for all", | "delivery_staffPlaceholder": "Leave empty for all", | ||||
| @@ -286,12 +286,13 @@ | |||||
| "code.joStatus.storing": "Storing", | "code.joStatus.storing": "Storing", | ||||
| "code.joStatus.PARTIAL": "Partial", | "code.joStatus.PARTIAL": "Partial", | ||||
| "code.joStatus.partial": "Partial", | "code.joStatus.partial": "Partial", | ||||
| "code.productionStatus.Pass": "Pass", | |||||
| "code.productionStatus.Pass": "Skip", | |||||
| "code.productionStatus.Completed": "Completed", | "code.productionStatus.Completed": "Completed", | ||||
| "code.productionStatus.Pending": "Pending", | "code.productionStatus.Pending": "Pending", | ||||
| "code.productionStatus.Paused": "Paused", | "code.productionStatus.Paused": "Paused", | ||||
| "code.productionStatus.InProgress": "In progress", | "code.productionStatus.InProgress": "In progress", | ||||
| "code.productionStatus.Skip": "Skip", | "code.productionStatus.Skip": "Skip", | ||||
| "code.productionStatus.autoPass": "Auto Skipped", | |||||
| "continuousScanBlocked": "Finish current scan first", | "continuousScanBlocked": "Finish current scan first", | ||||
| "nodeJoOut": "Job order material issue", | "nodeJoOut": "Job order material issue", | ||||
| "nodePoOut": "Purchase pick", | "nodePoOut": "Purchase pick", | ||||
| @@ -272,7 +272,9 @@ | |||||
| "Overview": "Overview", | "Overview": "Overview", | ||||
| "Packaging": "Packaging", | "Packaging": "Packaging", | ||||
| "Partial quantity submitted. Please submit more or complete the order.": "Partial quantity submitted. Please submit more or complete the order.", | "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", | "Passed Step": "Passed Step", | ||||
| "Pause": "Pause", | "Pause": "Pause", | ||||
| "Pause Reason": "Pause Reason", | "Pause Reason": "Pause Reason", | ||||
| @@ -123,7 +123,8 @@ | |||||
| "Job process detail: handler": "Handler", | "Job process detail: handler": "Handler", | ||||
| "Job process detail: process name": "Process Name", | "Job process detail: process name": "Process Name", | ||||
| "Job process detail: time": "Time", | "Job process detail: time": "Time", | ||||
| "Just Pass": "Just Pass", | |||||
| "Just Pass": "Skip", | |||||
| "Auto Pass": "Auto Skipped", | |||||
| "Last updated": "Last updated", | "Last updated": "Last updated", | ||||
| "Lines with insufficient stock: ": "Lines with insufficient stock: ", | "Lines with insufficient stock: ": "Lines with insufficient stock: ", | ||||
| "Lines with sufficient stock: ": "Lines with sufficient 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 name": "Failed to search by name", | ||||
| "Failed to search by username": "Failed to search by username", | "Failed to search by username": "Failed to search by username", | ||||
| "Staff No is required": "Staff No is required", | "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": "工序即時看板", | "board_processLive": "工序即時看板", | ||||
| "dateRange_lastDays": "最近 {{d}} 天", | "dateRange_lastDays": "最近 {{d}} 天", | ||||
| "delivery_colAvgMin": "平均分鐘/單", | "delivery_colAvgMin": "平均分鐘/單", | ||||
| "delivery_colItemKindCount": "總揀貨款數", | |||||
| "delivery_colItemQtyPicked": "揀貨數量", | |||||
| "delivery_colPickCount": "揀單數", | "delivery_colPickCount": "揀單數", | ||||
| "delivery_colStaff": "員工", | "delivery_colStaff": "員工", | ||||
| "delivery_colTotalMin": "總分鐘", | "delivery_colTotalMin": "總分鐘", | ||||
| @@ -19,7 +21,7 @@ | |||||
| "delivery_ordersByDate": "按日期發貨單數量", | "delivery_ordersByDate": "按日期發貨單數量", | ||||
| "delivery_ordersByDate_export": "發貨單數量_按日期", | "delivery_ordersByDate_export": "發貨單數量_按日期", | ||||
| "delivery_staff": "員工", | "delivery_staff": "員工", | ||||
| "delivery_staffPerfCaption": "週期內每人揀單數及總耗時(首揀至完成)", | |||||
| "delivery_staffPerfCaption": "週期內每人揀單數、總揀貨款數、揀貨數量及總耗時(首揀至完成)", | |||||
| "delivery_staffPerfDateError": "員工發貨績效的起始日期不能晚於結束日期", | "delivery_staffPerfDateError": "員工發貨績效的起始日期不能晚於結束日期", | ||||
| "delivery_staffPerformanceTitle": "員工發貨績效(每日揀貨數量與耗時)", | "delivery_staffPerformanceTitle": "員工發貨績效(每日揀貨數量與耗時)", | ||||
| "delivery_staffPlaceholder": "不選則全部", | "delivery_staffPlaceholder": "不選則全部", | ||||
| @@ -286,12 +286,13 @@ | |||||
| "code.joStatus.storing": "待QC上架", | "code.joStatus.storing": "待QC上架", | ||||
| "code.joStatus.PARTIAL": "部分完成", | "code.joStatus.PARTIAL": "部分完成", | ||||
| "code.joStatus.partial": "部分完成", | "code.joStatus.partial": "部分完成", | ||||
| "code.productionStatus.Pass": "通過", | |||||
| "code.productionStatus.Pass": "跳過", | |||||
| "code.productionStatus.Completed": "完成", | "code.productionStatus.Completed": "完成", | ||||
| "code.productionStatus.Pending": "待處理", | "code.productionStatus.Pending": "待處理", | ||||
| "code.productionStatus.Paused": "已暫停", | "code.productionStatus.Paused": "已暫停", | ||||
| "code.productionStatus.InProgress": "進行中", | "code.productionStatus.InProgress": "進行中", | ||||
| "code.productionStatus.Skip": "跳過", | "code.productionStatus.Skip": "跳過", | ||||
| "code.productionStatus.autoPass": "已自動跳過", | |||||
| "continuousScanBlocked": "請先完成目前掃描", | "continuousScanBlocked": "請先完成目前掃描", | ||||
| "nodeJoOut": "工單提料", | "nodeJoOut": "工單提料", | ||||
| "nodePoOut": "採購提料", | "nodePoOut": "採購提料", | ||||
| @@ -10,7 +10,8 @@ | |||||
| "Actual Pick Qty": "實際提料數量", | "Actual Pick Qty": "實際提料數量", | ||||
| "Add Bag": "新增包裝袋", | "Add Bag": "新增包裝袋", | ||||
| "Add Record": "添加記錄", | "Add Record": "添加記錄", | ||||
| "Just Pass": "通過", | |||||
| "Just Pass": "跳過", | |||||
| "Auto Pass": "已自動跳過", | |||||
| "Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中", | "Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中", | ||||
| "Add some entries!": "請添加條目", | "Add some entries!": "請添加條目", | ||||
| "All": "全部", | "All": "全部", | ||||
| @@ -275,7 +276,7 @@ | |||||
| "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": "跳過", | |||||
| "Passed Step": "通過步驟", | "Passed Step": "通過步驟", | ||||
| "Pause": "暫停", | "Pause": "暫停", | ||||
| "Pause Reason": "暫停原因", | "Pause Reason": "暫停原因", | ||||
| @@ -32,7 +32,7 @@ | |||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Confirm cancel job order": "確認取消工單", | "Confirm cancel job order": "確認取消工單", | ||||
| "Confirm delete job order": "確認刪除工單", | "Confirm delete job order": "確認刪除工單", | ||||
| "Confirm to Pass this Process?": "確認要通過此工序嗎?", | |||||
| "Confirm to Pass this Process?": "確認要跳過此工序嗎?", | |||||
| "Confirm to update this Job Order?": "確認要完成此工單嗎?", | "Confirm to update this Job Order?": "確認要完成此工單嗎?", | ||||
| "Consumed Qty": "消耗數量", | "Consumed Qty": "消耗數量", | ||||
| "Continue": "繼續", | "Continue": "繼續", | ||||
| @@ -123,7 +123,8 @@ | |||||
| "Job process detail: handler": "員工", | "Job process detail: handler": "員工", | ||||
| "Job process detail: process name": "工序", | "Job process detail: process name": "工序", | ||||
| "Job process detail: time": "時間", | "Job process detail: time": "時間", | ||||
| "Just Pass": "已完成", | |||||
| "Just Pass": "跳過", | |||||
| "Auto Pass": "已自動跳過", | |||||
| "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": "用戶不存在", | |||||
| "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": "伺服器錯誤,請稍後再試。" | |||||
| } | } | ||||