Parcourir la source

Merge branch 'MergeProblem1' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend into MergeProblem1

MergeProblem1
CANCERYS\kw093 il y a 3 jours
Parent
révision
f58875a3e9
2 fichiers modifiés avec 159 ajouts et 11 suppressions
  1. +157
    -11
      src/app/(main)/ps/page.tsx
  2. +2
    -0
      src/app/api/pickOrder/actions.ts

+ 157
- 11
src/app/(main)/ps/page.tsx Voir le fichier

@@ -1,6 +1,6 @@
"use client";

import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect, useMemo, useRef } from "react";
import Search from "@mui/icons-material/Search";
import Visibility from "@mui/icons-material/Visibility";
import FormatListNumbered from "@mui/icons-material/FormatListNumbered";
@@ -14,6 +14,8 @@ import PageTitleBar from "@/components/PageTitleBar";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { exportChartToXlsx } from "@/app/(main)/chart/_components/exportChartToXlsx";
import * as XLSX from "xlsx";

type ItemDailyOutRow = {
itemCode: string;
@@ -56,6 +58,8 @@ export default function ProductionSchedulePage() {
const [coffeeOrTeaUpdating, setCoffeeOrTeaUpdating] = useState<string | null>(null);
const [fakeOnHandSavingCode, setFakeOnHandSavingCode] = useState<string | null>(null);
const [fakeOnHandClearingCode, setFakeOnHandClearingCode] = useState<string | null>(null);
const [isImportingFakeOnHand, setIsImportingFakeOnHand] = useState(false);
const itemDailyOutRequestRef = useRef(0);

useEffect(() => {
handleSearch();
@@ -209,7 +213,13 @@ export default function ProductionSchedulePage() {
const fromDateDefault = dayjs().subtract(29, "day").format("YYYY-MM-DD");
const toDateDefault = dayjs().format("YYYY-MM-DD");

const fetchItemDailyOut = async () => {
const fetchItemDailyOut = async (force: boolean = false) => {
// Avoid starting a new fetch while an import is in progress,
// unless explicitly forced (after a successful import).
if (!force && isImportingFakeOnHand) return;

const currentReq = itemDailyOutRequestRef.current + 1;
itemDailyOutRequestRef.current = currentReq;
setItemDailyOutLoading(true);
try {
const params = new URLSearchParams({
@@ -222,6 +232,10 @@ export default function ProductionSchedulePage() {
);
if (response.status === 401 || response.status === 403) return;
const data = await response.json();
// If a newer request has started, ignore this response to avoid overwriting with stale data
if (itemDailyOutRequestRef.current !== currentReq) {
return;
}
const rows: ItemDailyOutRow[] = (Array.isArray(data) ? data : []).map(
(r: any) => ({
itemCode: r.itemCode ?? "",
@@ -248,7 +262,10 @@ export default function ProductionSchedulePage() {
console.error("itemDailyOut Error:", e);
setItemDailyOutList([]);
} finally {
setItemDailyOutLoading(false);
// Only clear loading state if this is the latest request
if (itemDailyOutRequestRef.current === currentReq) {
setItemDailyOutLoading(false);
}
}
};

@@ -257,6 +274,107 @@ export default function ProductionSchedulePage() {
fetchItemDailyOut();
};

/** Download current fake on-hand overrides (item_fake_onhand) as Excel template. */
const handleExportFakeOnHand = () => {
const rows = itemDailyOutList
.filter((row) => row.fakeOnHandQty != null)
.map((row) => ({
itemCode: row.itemCode,
onHandQty: row.fakeOnHandQty,
}));
exportChartToXlsx(rows, "item_fake_onhand", "item_fake_onhand");
};

/** Upload Excel and bulk update item_fake_onhand via /ps/setFakeOnHand. */
const handleImportFakeOnHand = async (file: File) => {
try {
setIsImportingFakeOnHand(true);

const data = await file.arrayBuffer();
const workbook = XLSX.read(data, { type: "array" });
const sheetName = workbook.SheetNames[0];
if (!sheetName) {
alert("Excel 沒有工作表。");
return;
}
const sheet = workbook.Sheets[sheetName];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: null });
if (!rows.length) {
alert("Excel 內容為空。");
return;
}

// Build allowed itemCodes (BOM scope) from current list
const allowedCodes = new Set(itemDailyOutList.map((r) => r.itemCode));

const invalidCodes: string[] = [];

// Map Excel rows to backend payload format
const payload = rows
.map((row) => {
const itemCode = (row.itemCode ?? row.ItemCode ?? row["Item Code"])?.toString().trim();
if (!itemCode) return null;
if (!allowedCodes.has(itemCode)) {
invalidCodes.push(itemCode);
}
const rawQty = row.onHandQty ?? row.OnHandQty ?? row["On Hand Qty"];
const qtyNum =
rawQty === null || rawQty === "" || typeof rawQty === "undefined"
? null
: Number(rawQty);
return { itemCode, onHandQty: qtyNum };
})
.filter((r): r is { itemCode: string; onHandQty: number | null } => r !== null);

if (!payload.length) {
alert("找不到任何有效的 itemCode。");
return;
}

// Warn user about itemCodes that are not in BOM scope (won't affect forecast)
if (invalidCodes.length) {
const preview = invalidCodes.slice(0, 10).join(", ");
alert(
`注意:以下物料編號不在排期 BOM 範圍內,預測不會受影響,只會寫入覆蓋表。\n\n` +
`${preview}${invalidCodes.length > 10 ? ` 等共 ${invalidCodes.length} 筆` : ""}`
);
}

const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/importFakeOnHand`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});

if (resp.status === 401 || resp.status === 403) {
alert("登入已過期或沒有權限,請重新登入後再試。");
return;
}

if (!resp.ok) {
const msg = await resp.text().catch(() => "");
alert(
`匯入失敗(狀態碼 ${resp.status})。${
msg ? `\n伺服器訊息:${msg.slice(0, 120)}` : ""
}`
);
return;
}

const result = await resp.json().catch(() => ({ count: payload.length }));

// Backend clears item_fake_onhand then inserts payload rows,
// so after success the table should exactly match the uploaded Excel.
await fetchItemDailyOut(true);
alert(`已成功匯入並更新 ${result.count ?? payload.length} 筆排期庫存 (item_fake_onhand)。`);
} catch (e) {
console.error("Import fake on hand error:", e);
alert("匯入失敗,請檢查檔案格式。");
} finally {
setIsImportingFakeOnHand(false);
}
};

const handleSaveDailyQty = async (itemCode: string, dailyQty: number) => {
setDailyOutSavingCode(itemCode);
try {
@@ -812,16 +930,44 @@ export default function ProductionSchedulePage() {
/>
<div className="relative z-10 flex max-h-[90vh] w-full max-w-6xl flex-col overflow-hidden rounded-lg border border-slate-200 bg-white shadow-xl dark:border-slate-700 dark:bg-slate-800">
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-100 px-4 py-3 dark:border-slate-700 dark:bg-slate-700/50">
<h2 id="settings-panel-title" className="text-lg font-semibold text-slate-900 dark:text-white">
<h2
id="settings-panel-title"
className="text-lg font-semibold text-slate-900 dark:text-white"
>
排期設定
</h2>
<button
type="button"
onClick={() => setIsDailyOutPanelOpen(false)}
className="rounded p-1 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:hover:bg-slate-600 dark:hover:text-slate-200"
>
關閉
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleExportFakeOnHand}
className="inline-flex items-center gap-1 rounded border border-amber-500/80 bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700 shadow-sm hover:bg-amber-100 dark:border-amber-400/80 dark:bg-slate-800 dark:text-amber-300 dark:hover:bg-amber-500/20"
>
匯出排期庫存 Excel
</button>
<label className="inline-flex cursor-pointer items-center gap-1 rounded border border-blue-500/70 bg-white px-3 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-blue-50 dark:border-blue-500/50 dark:bg-slate-800 dark:text-blue-400 dark:hover:bg-blue-500/10">
<span className="inline-flex h-2 w-2 rounded-full bg-amber-500" />
<span className="text-amber-700 dark:text-amber-300">匯入排期庫存 Excel(覆蓋設定)</span>
<input
type="file"
accept=".xlsx,.xls"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
void handleImportFakeOnHand(file);
e.target.value = "";
}
}}
/>
</label>
<button
type="button"
onClick={() => setIsDailyOutPanelOpen(false)}
className="rounded p-1 text-slate-500 hover:bg-slate-200 hover:text-slate-700 dark:hover:bg-slate-600 dark:hover:text-slate-200"
>
關閉
</button>
</div>
</div>
<p className="px-4 py-2 text-sm text-slate-600 dark:text-slate-400">
預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。


+ 2
- 0
src/app/api/pickOrder/actions.ts Voir le fichier

@@ -208,6 +208,8 @@ export interface PickExecutionIssueData {
missQty: number;
badItemQty: number;
badPackageQty?: number;
/** Optional: frontend-only reference to stock_out_line.id for the picked lot. */
stockOutLineId?: number;
issueRemark: string;
pickerName: string;
handledBy?: number;


Chargement…
Annuler
Enregistrer