Selaa lähdekoodia

1. 員工發貨績效圖表

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 log
fix負數倉
CANCERYS\kw093 1 kuukausi sitten
vanhempi
commit
986edbf6d4
29 muutettua tiedostoa jossa 334 lisäystä ja 104 poistoa
  1. +6
    -2
      scripts/update-chart-i18n.js
  2. +39
    -6
      src/app/(main)/chart/delivery/page.tsx
  3. +3
    -0
      src/app/(main)/report/ReportSelectionDashboard.tsx
  4. +27
    -0
      src/app/(main)/report/page.tsx
  5. +1
    -1
      src/app/(main)/report/reportCategories.ts
  6. +34
    -28
      src/app/(main)/report/shopOrderReplenishmentReportApi.ts
  7. +5
    -0
      src/app/api/chart/client.ts
  8. +10
    -1
      src/app/api/user/client.ts
  9. +31
    -4
      src/components/CreateUser/CreateUser.tsx
  10. +5
    -0
      src/components/CreateUser/UserDetail.tsx
  11. +2
    -2
      src/components/JoSearch/JoCreateFormModal.tsx
  12. +4
    -4
      src/components/JoSearch/JoSearch.tsx
  13. +4
    -4
      src/components/JoWorkbench/JoWorkbenchSearch.tsx
  14. +36
    -26
      src/components/JoWorkbench/newJobPickExecution.tsx
  15. +6
    -3
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  16. +4
    -4
      src/components/ProductionProcess/ProductionProcessList.tsx
  17. +28
    -6
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx
  18. +23
    -1
      src/components/UserSearch/UserExcelSheetView.tsx
  19. +29
    -0
      src/config/reportConfig.ts
  20. +3
    -1
      src/i18n/en/chart.json
  21. +2
    -1
      src/i18n/en/itemTracing.json
  22. +3
    -1
      src/i18n/en/jo.json
  23. +2
    -1
      src/i18n/en/productionProcess.json
  24. +8
    -1
      src/i18n/en/user.json
  25. +3
    -1
      src/i18n/zh/chart.json
  26. +2
    -1
      src/i18n/zh/itemTracing.json
  27. +3
    -2
      src/i18n/zh/jo.json
  28. +3
    -2
      src/i18n/zh/productionProcess.json
  29. +8
    -1
      src/i18n/zh/user.json

+ 6
- 2
scripts/update-chart-i18n.js Näytä tiedosto

@@ -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": "每日按員工單數",


+ 39
- 6
src/app/(main)/chart/delivery/page.tsx Näytä tiedosto

@@ -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>


+ 3
- 0
src/app/(main)/report/ReportSelectionDashboard.tsx Näytä tiedosto

@@ -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,


+ 27
- 0
src/app/(main)/report/page.tsx Näytä tiedosto

@@ -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',


+ 1
- 1
src/app/(main)/report/reportCategories.ts Näytä tiedosto

@@ -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",


+ 34
- 28
src/app/(main)/report/shopOrderReplenishmentReportApi.ts Näytä tiedosto

@@ -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(


+ 5
- 0
src/app/api/chart/client.ts Näytä tiedosto

@@ -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),
};
});
}


+ 10
- 1
src/app/api/user/client.ts Näytä tiedosto

@@ -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}` : ""}`,
);
}
};

+ 31
- 4
src/components/CreateUser/CreateUser.tsx Näytä tiedosto

@@ -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"


+ 5
- 0
src/components/CreateUser/UserDetail.tsx Näytä tiedosto

@@ -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}>


+ 2
- 2
src/components/JoSearch/JoCreateFormModal.tsx Näytä tiedosto

@@ -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) {


+ 4
- 4
src/components/JoSearch/JoSearch.tsx Näytä tiedosto

@@ -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([]);


+ 4
- 4
src/components/JoWorkbench/JoWorkbenchSearch.tsx Näytä tiedosto

@@ -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([]);


+ 36
- 26
src/components/JoWorkbench/newJobPickExecution.tsx Näytä tiedosto

@@ -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;


+ 6
- 3
src/components/ProductionProcess/ProductionProcessDetail.tsx Näytä tiedosto

@@ -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" />
) : (


+ 4
- 4
src/components/ProductionProcess/ProductionProcessList.tsx Näytä tiedosto

@@ -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) => {


+ 28
- 6
src/components/ProductionProcess/ProductionProcessStepExecution.tsx Näytä tiedosto

@@ -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">


+ 23
- 1
src/components/UserSearch/UserExcelSheetView.tsx Näytä tiedosto

@@ -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;


+ 29
- 0
src/config/reportConfig.ts Näytä tiedosto

@@ -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" },
],
},
],
},
]

+ 3
- 1
src/i18n/en/chart.json Näytä tiedosto

@@ -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",


+ 2
- 1
src/i18n/en/itemTracing.json Näytä tiedosto

@@ -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",


+ 3
- 1
src/i18n/en/jo.json Näytä tiedosto

@@ -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",


+ 2
- 1
src/i18n/en/productionProcess.json Näytä tiedosto

@@ -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: ",


+ 8
- 1
src/i18n/en/user.json Näytä tiedosto

@@ -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."
}

+ 3
- 1
src/i18n/zh/chart.json Näytä tiedosto

@@ -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": "不選則全部",


+ 2
- 1
src/i18n/zh/itemTracing.json Näytä tiedosto

@@ -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": "採購提料",


+ 3
- 2
src/i18n/zh/jo.json Näytä tiedosto

@@ -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": "暫停原因",


+ 3
- 2
src/i18n/zh/productionProcess.json Näytä tiedosto

@@ -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: ": "可提料項目數量: ",


+ 8
- 1
src/i18n/zh/user.json Näytä tiedosto

@@ -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": "伺服器錯誤,請稍後再試。"
}

Ladataan…
Peruuta
Tallenna