From 190d78c6df688b9c7cf8234f6f3ba6f8220b1772 Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Sat, 14 Mar 2026 18:14:06 +0800 Subject: [PATCH] adding PS settings --- src/app/(main)/ps/page.tsx | 471 +++++++++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index 675380c..de3b1be 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -7,12 +7,27 @@ import FormatListNumbered from "@mui/icons-material/FormatListNumbered"; import ShowChart from "@mui/icons-material/ShowChart"; import Download from "@mui/icons-material/Download"; import Hub from "@mui/icons-material/Hub"; +import Settings from "@mui/icons-material/Settings"; +import Clear from "@mui/icons-material/Clear"; import { CircularProgress } from "@mui/material"; import PageTitleBar from "@/components/PageTitleBar"; import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +type ItemDailyOutRow = { + itemCode: string; + itemName: string; + unit?: string; + onHandQty?: number | null; + fakeOnHandQty?: number | null; + avgQtyLastMonth?: number; + dailyQty?: number | null; + isCoffee?: number; + isTea?: number; + isLemon?: number; +}; + export default function ProductionSchedulePage() { const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD")); const [schedules, setSchedules] = useState([]); @@ -33,6 +48,15 @@ export default function ProductionSchedulePage() { dayjs().format("YYYY-MM-DD") ); + const [isDailyOutPanelOpen, setIsDailyOutPanelOpen] = useState(false); + const [itemDailyOutList, setItemDailyOutList] = useState([]); + const [itemDailyOutLoading, setItemDailyOutLoading] = useState(false); + const [dailyOutSavingCode, setDailyOutSavingCode] = useState(null); + const [dailyOutClearingCode, setDailyOutClearingCode] = useState(null); + const [coffeeOrTeaUpdating, setCoffeeOrTeaUpdating] = useState(null); + const [fakeOnHandSavingCode, setFakeOnHandSavingCode] = useState(null); + const [fakeOnHandClearingCode, setFakeOnHandClearingCode] = useState(null); + useEffect(() => { handleSearch(); }, []); @@ -182,12 +206,228 @@ export default function ProductionSchedulePage() { } }; + const fromDateDefault = dayjs().subtract(29, "day").format("YYYY-MM-DD"); + const toDateDefault = dayjs().format("YYYY-MM-DD"); + + const fetchItemDailyOut = async () => { + setItemDailyOutLoading(true); + try { + const params = new URLSearchParams({ + fromDate: fromDateDefault, + toDate: toDateDefault, + }); + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/itemDailyOut.json?${params.toString()}`, + { method: "GET" } + ); + if (response.status === 401 || response.status === 403) return; + const data = await response.json(); + const rows: ItemDailyOutRow[] = (Array.isArray(data) ? data : []).map( + (r: any) => ({ + itemCode: r.itemCode ?? "", + itemName: r.itemName ?? "", + unit: r.unit != null ? String(r.unit) : "", + onHandQty: r.onHandQty != null ? Number(r.onHandQty) : null, + fakeOnHandQty: + r.fakeOnHandQty != null && r.fakeOnHandQty !== "" + ? Number(r.fakeOnHandQty) + : null, + avgQtyLastMonth: + r.avgQtyLastMonth != null ? Number(r.avgQtyLastMonth) : undefined, + dailyQty: + r.dailyQty != null && r.dailyQty !== "" + ? Number(r.dailyQty) + : null, + isCoffee: r.isCoffee != null ? Number(r.isCoffee) : 0, + isTea: r.isTea != null ? Number(r.isTea) : 0, + isLemon: r.isLemon != null ? Number(r.isLemon) : 0, + }) + ); + setItemDailyOutList(rows); + } catch (e) { + console.error("itemDailyOut Error:", e); + setItemDailyOutList([]); + } finally { + setItemDailyOutLoading(false); + } + }; + + const openSettingsPanel = () => { + setIsDailyOutPanelOpen(true); + fetchItemDailyOut(); + }; + + const handleSaveDailyQty = async (itemCode: string, dailyQty: number) => { + setDailyOutSavingCode(itemCode); + try { + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/setDailyQtyOut`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ itemCode, dailyQty }), + } + ); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { + setItemDailyOutList((prev) => + prev.map((r) => + r.itemCode === itemCode ? { ...r, dailyQty } : r + ) + ); + } else { + alert("儲存失敗"); + } + } catch (e) { + console.error("setDailyQtyOut Error:", e); + alert("儲存失敗"); + } finally { + setDailyOutSavingCode(null); + } + }; + + const handleClearDailyQty = async (itemCode: string) => { + if (!confirm(`確定要清除${itemCode}的設定排期每天出貨量嗎?`)) return; + setDailyOutClearingCode(itemCode); + try { + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/clearDailyQtyOut`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ itemCode }), + } + ); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { + setItemDailyOutList((prev) => + prev.map((r) => + r.itemCode === itemCode ? { ...r, dailyQty: null } : r + ) + ); + } else { + alert("清除失敗"); + } + } catch (e) { + console.error("clearDailyQtyOut Error:", e); + alert("清除失敗"); + } finally { + setDailyOutClearingCode(null); + } + }; + + const handleSetCoffeeOrTea = async ( + itemCode: string, + systemType: "coffee" | "tea" | "lemon", + enabled: boolean + ) => { + const key = `${itemCode}-${systemType}`; + setCoffeeOrTeaUpdating(key); + try { + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/setCoffeeOrTea`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ itemCode, systemType, enabled }), + } + ); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { + setItemDailyOutList((prev) => + prev.map((r) => { + if (r.itemCode !== itemCode) return r; + const next = { ...r }; + if (systemType === "coffee") next.isCoffee = enabled ? 1 : 0; + if (systemType === "tea") next.isTea = enabled ? 1 : 0; + if (systemType === "lemon") next.isLemon = enabled ? 1 : 0; + return next; + }) + ); + } else { + alert("設定失敗"); + } + } catch (e) { + console.error("setCoffeeOrTea Error:", e); + alert("設定失敗"); + } finally { + setCoffeeOrTeaUpdating(null); + } + }; + + const handleSetFakeOnHand = async (itemCode: string, onHandQty: number) => { + setFakeOnHandSavingCode(itemCode); + try { + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/setFakeOnHand`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ itemCode, onHandQty }), + } + ); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { + setItemDailyOutList((prev) => + prev.map((r) => + r.itemCode === itemCode ? { ...r, fakeOnHandQty: onHandQty } : r + ) + ); + } else { + alert("設定失敗"); + } + } catch (e) { + console.error("setFakeOnHand Error:", e); + alert("設定失敗"); + } finally { + setFakeOnHandSavingCode(null); + } + }; + + const handleClearFakeOnHand = async (itemCode: string) => { + if (!confirm("確定要清除此物料的設定排期庫存嗎?")) return; + setFakeOnHandClearingCode(itemCode); + try { + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/setFakeOnHand`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ itemCode, onHandQty: null }), + } + ); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { + setItemDailyOutList((prev) => + prev.map((r) => + r.itemCode === itemCode ? { ...r, fakeOnHandQty: null } : r + ) + ); + } else { + alert("清除失敗"); + } + } catch (e) { + console.error("clearFakeOnHand Error:", e); + alert("清除失敗"); + } finally { + setFakeOnHandClearingCode(null); + } + }; + return (
+
)} + + {/* 排期設定 Dialog */} + {isDailyOutPanelOpen && ( +
+
setIsDailyOutPanelOpen(false)} + /> +
+
+

+ 排期設定 +

+ +
+

+ 預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。 +

+
+ {itemDailyOutLoading ? ( +
+ +
+ ) : ( + + + + + + + + + + + + + + + + + {itemDailyOutList.map((row, idx) => ( + + ))} + +
物料編號物料名稱單位庫存設定排期庫存過去平均出貨量設定排期每天出貨量咖啡檸檬
+ )} +
+
+
+ )}
); } + +function DailyOutRow({ + row, + onSave, + onClear, + onSetCoffeeOrTea, + onSetFakeOnHand, + onClearFakeOnHand, + saving, + clearing, + coffeeOrTeaUpdating, + fakeOnHandSaving, + fakeOnHandClearing, + formatNum, +}: { + row: ItemDailyOutRow; + onSave: (itemCode: string, dailyQty: number) => void; + onClear: (itemCode: string) => void; + onSetCoffeeOrTea: (itemCode: string, systemType: "coffee" | "tea" | "lemon", enabled: boolean) => void; + onSetFakeOnHand: (itemCode: string, onHandQty: number) => void; + onClearFakeOnHand: (itemCode: string) => void; + saving: boolean; + clearing: boolean; + coffeeOrTeaUpdating: string | null; + fakeOnHandSaving: boolean; + fakeOnHandClearing: boolean; + formatNum: (n: any) => string; +}) { + const [editQty, setEditQty] = useState( + row.dailyQty != null ? String(row.dailyQty) : "" + ); + const [editFakeOnHand, setEditFakeOnHand] = useState( + row.fakeOnHandQty != null ? String(row.fakeOnHandQty) : "" + ); + useEffect(() => { + setEditQty(row.dailyQty != null ? String(row.dailyQty) : ""); + }, [row.dailyQty]); + useEffect(() => { + setEditFakeOnHand(row.fakeOnHandQty != null ? String(row.fakeOnHandQty) : ""); + }, [row.fakeOnHandQty]); + const numVal = parseFloat(editQty); + const isValid = !Number.isNaN(numVal) && numVal >= 0; + const hasSetQty = row.dailyQty != null; + const fakeOnHandNum = parseFloat(editFakeOnHand); + const isValidFakeOnHand = !Number.isNaN(fakeOnHandNum) && fakeOnHandNum >= 0; + const hasSetFakeOnHand = row.fakeOnHandQty != null; + const isCoffee = (row.isCoffee ?? 0) > 0; + const isTea = (row.isTea ?? 0) > 0; + const isLemon = (row.isLemon ?? 0) > 0; + const updatingCoffee = coffeeOrTeaUpdating === `${row.itemCode}-coffee`; + const updatingTea = coffeeOrTeaUpdating === `${row.itemCode}-tea`; + const updatingLemon = coffeeOrTeaUpdating === `${row.itemCode}-lemon`; + return ( + + {row.itemCode} + {row.itemName} + {row.unit ?? ""} + {formatNum(row.onHandQty)} + +
+ setEditFakeOnHand(e.target.value)} + onBlur={() => { + if (isValidFakeOnHand) onSetFakeOnHand(row.itemCode, fakeOnHandNum); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && isValidFakeOnHand) onSetFakeOnHand(row.itemCode, fakeOnHandNum); + }} + className="w-24 rounded border border-slate-300 bg-white px-2 py-1 text-left text-slate-900 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" + /> + {hasSetFakeOnHand && ( + + )} +
+ + {formatNum(row.avgQtyLastMonth)} + +
+ setEditQty(e.target.value)} + onBlur={() => { + if (isValid) onSave(row.itemCode, numVal); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && isValid) onSave(row.itemCode, numVal); + }} + className="w-24 rounded border border-slate-300 bg-white px-2 py-1 text-left text-slate-900 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" + /> + {hasSetQty && ( + + )} +
+ + + + + + + + + + + + ); +}