@@ -11,6 +11,7 @@ import Settings from "@mui/icons-material/Settings";
import Clear from "@mui/icons-material/Clear";
import Clear from "@mui/icons-material/Clear";
import Refresh from "@mui/icons-material/Refresh";
import Refresh from "@mui/icons-material/Refresh";
import { CircularProgress } from "@mui/material";
import { CircularProgress } from "@mui/material";
import { useTranslation } from "react-i18next";
import PageTitleBar from "@/components/PageTitleBar";
import PageTitleBar from "@/components/PageTitleBar";
import dayjs from "dayjs";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
@@ -33,6 +34,7 @@ type ItemDailyOutRow = {
};
};
export default function ProductionSchedulePage() {
export default function ProductionSchedulePage() {
const { t } = useTranslation("scheduling");
const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD"));
const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD"));
const [schedules, setSchedules] = useState<any[]>([]);
const [schedules, setSchedules] = useState<any[]>([]);
const [selectedLines, setSelectedLines] = useState<any[]>([]);
const [selectedLines, setSelectedLines] = useState<any[]>([]);
@@ -111,7 +113,7 @@ export default function ProductionSchedulePage() {
const handleConfirmForecast = async () => {
const handleConfirmForecast = async () => {
if (!forecastStartDate || forecastDays === "" || forecastDays < 1) {
if (!forecastStartDate || forecastDays === "" || forecastDays < 1) {
alert("Please enter a valid start date and number of days (≥1)." );
alert(t("forecastInvalid") );
return;
return;
}
}
setLoading(true);
setLoading(true);
@@ -126,15 +128,15 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.status === 401 || response.status === 403) return;
if (response.ok) {
if (response.ok) {
await handleSearch();
await handleSearch();
alert("成功計算排期!" );
alert(t("forecastOk") );
} else {
} else {
const errorText = await response.text();
const errorText = await response.text();
console.error("Forecast failed:", errorText);
console.error("Forecast failed:", errorText);
alert(`計算錯誤: ${response.status} - ${errorText.substring(0, 120)}` );
alert(t("forecastError", { status: response.status, detail: errorText.substring(0, 120) }) );
}
}
} catch (e) {
} catch (e) {
console.error("Forecast Error:", e);
console.error("Forecast Error:", e);
alert("發生不明狀況." );
alert(t("forecastUnknown") );
} finally {
} finally {
setLoading(false);
setLoading(false);
}
}
@@ -143,11 +145,11 @@ export default function ProductionSchedulePage() {
const handleConfirmDoQtyExport = async () => {
const handleConfirmDoQtyExport = async () => {
if (doQtyExportInFlightRef.current) return;
if (doQtyExportInFlightRef.current) return;
if (!doQtyExportFromDate || !doQtyExportToDate) {
if (!doQtyExportFromDate || !doQtyExportToDate) {
alert("請選擇開始及結束日期。" );
alert(t("doQtyDatesRequired") );
return;
return;
}
}
if (dayjs(doQtyExportFromDate).isAfter(dayjs(doQtyExportToDate))) {
if (dayjs(doQtyExportFromDate).isAfter(dayjs(doQtyExportToDate))) {
alert("開始日期不可晚於結束日期。" );
alert(t("doQtyDateOrder") );
return;
return;
}
}
doQtyExportInFlightRef.current = true;
doQtyExportInFlightRef.current = true;
@@ -178,7 +180,7 @@ export default function ProductionSchedulePage() {
document.body.removeChild(a);
document.body.removeChild(a);
} catch (e) {
} catch (e) {
console.error("DO qty export Error:", e);
console.error("DO qty export Error:", e);
alert("匯出送貨單數量失敗。" );
alert(t("doQtyExportFail") );
} finally {
} finally {
setIsDoQtyExporting(false);
setIsDoQtyExporting(false);
doQtyExportInFlightRef.current = false;
doQtyExportInFlightRef.current = false;
@@ -187,7 +189,7 @@ export default function ProductionSchedulePage() {
const handleConfirmExport = async () => {
const handleConfirmExport = async () => {
if (!exportFromDate) {
if (!exportFromDate) {
alert("Please select a from date." );
alert(t("exportFromRequired") );
return;
return;
}
}
setLoading(true);
setLoading(true);
@@ -211,7 +213,7 @@ export default function ProductionSchedulePage() {
document.body.removeChild(a);
document.body.removeChild(a);
} catch (e) {
} catch (e) {
console.error("Export Error:", e);
console.error("Export Error:", e);
alert("Failed to export file." );
alert(t("exportFail") );
} finally {
} finally {
setLoading(false);
setLoading(false);
}
}
@@ -219,7 +221,7 @@ export default function ProductionSchedulePage() {
const handleViewDetail = async (ps: any) => {
const handleViewDetail = async (ps: any) => {
if (!ps?.id) {
if (!ps?.id) {
alert("Cannot open details: missing schedule ID" );
alert(t("detailMissingId") );
return;
return;
}
}
setSelectedPs(ps);
setSelectedPs(ps);
@@ -230,7 +232,7 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
if (!response.ok) {
const errorText = await response.text().catch(() => "(no text)");
const errorText = await response.text().catch(() => "(no text)");
alert(`Server error ${response.status}: ${errorText}` );
alert(t("serverError", { status: response.status, detail: errorText }) );
return;
return;
}
}
const data = await response.json();
const data = await response.json();
@@ -238,7 +240,7 @@ export default function ProductionSchedulePage() {
setIsDetailOpen(true);
setIsDetailOpen(true);
} catch (err) {
} catch (err) {
console.error("Fetch failed:", err);
console.error("Fetch failed:", err);
alert("Network or fetch error – check console" );
alert(t("networkError") );
} finally {
} finally {
setLoading(false);
setLoading(false);
}
}
@@ -258,10 +260,15 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.status === 401 || response.status === 403) return;
if (response.ok) {
if (response.ok) {
const data = await response.json();
const data = await response.json();
alert(data.message || "Operation completed.");
const created = Number(data?.code);
if (data?.type === "psJobOrders" && Number.isFinite(created)) {
alert(created > 0 ? t("jobsCreated", { count: created }) : t("jobsNone"));
} else {
alert(data.message || t("operationDone"));
}
setIsDetailOpen(false);
setIsDetailOpen(false);
} else {
} else {
alert("Failed to generate jobs.");
alert(t("generateJobsFail") );
}
}
} catch (e) {
} catch (e) {
console.error("Release Error:", e);
console.error("Release Error:", e);
@@ -347,14 +354,17 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
if (!response.ok) {
const msg = await response.text().catch(() => "");
const msg = await response.text().catch(() => "");
alert(`刷新庫存失敗(${response.status})${msg ? `: ${msg.slice(0, 120)}` : ""}`);
alert(t("refreshStockFailStatus", {
status: response.status,
detail: msg ? `: ${msg.slice(0, 120)}` : "",
}));
return;
return;
}
}
await response.json().catch(() => null);
await response.json().catch(() => null);
await fetchItemDailyOut(true);
await fetchItemDailyOut(true);
} catch (e) {
} catch (e) {
console.error("refreshInventoryOnHand Error:", e);
console.error("refreshInventoryOnHand Error:", e);
alert("刷新庫存失敗。" );
alert(t("refreshStockFail") );
} finally {
} finally {
setIsRefreshingOnHand(false);
setIsRefreshingOnHand(false);
refreshOnHandInFlightRef.current = false;
refreshOnHandInFlightRef.current = false;
@@ -381,13 +391,13 @@ export default function ProductionSchedulePage() {
const workbook = XLSX.read(data, { type: "array" });
const workbook = XLSX.read(data, { type: "array" });
const sheetName = workbook.SheetNames[0];
const sheetName = workbook.SheetNames[0];
if (!sheetName) {
if (!sheetName) {
alert("Excel 沒有工作表。" );
alert(t("excelNoSheet") );
return;
return;
}
}
const sheet = workbook.Sheets[sheetName];
const sheet = workbook.Sheets[sheetName];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: null });
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: null });
if (!rows.length) {
if (!rows.length) {
alert("Excel 內容為空。" );
alert(t("excelEmpty") );
return;
return;
}
}
@@ -414,17 +424,18 @@ export default function ProductionSchedulePage() {
.filter((r): r is { itemCode: string; onHandQty: number | null } => r !== null);
.filter((r): r is { itemCode: string; onHandQty: number | null } => r !== null);
if (!payload.length) {
if (!payload.length) {
alert("找不到任何有效的 itemCode。" );
alert(t("excelNoItemCode") );
return;
return;
}
}
// Warn user about itemCodes that are not in BOM scope (won't affect forecast)
// Warn user about itemCodes that are not in BOM scope (won't affect forecast)
if (invalidCodes.length) {
if (invalidCodes.length) {
const preview = invalidCodes.slice(0, 10).join(", ");
const preview = invalidCodes.slice(0, 10).join(", ");
alert(
`注意:以下物料編號不在排期 BOM 範圍內,預測不會受影響,只會寫入覆蓋表。\n\n` +
`${preview}${invalidCodes.length > 10 ? ` 等共 ${invalidCodes.length} 筆` : ""}`
);
const more =
invalidCodes.length > 10
? t("importOutsideBomMore", { count: invalidCodes.length })
: "";
alert(t("importOutsideBom", { preview: `${preview}${more}` }));
}
}
const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/importFakeOnHand`, {
const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/importFakeOnHand`, {
@@ -434,17 +445,16 @@ export default function ProductionSchedulePage() {
});
});
if (resp.status === 401 || resp.status === 403) {
if (resp.status === 401 || resp.status === 403) {
alert("登入已過期或沒有權限,請重新登入後再試。" );
alert(t("sessionExpired") );
return;
return;
}
}
if (!resp.ok) {
if (!resp.ok) {
const msg = await resp.text().catch(() => "");
const msg = await resp.text().catch(() => "");
alert(
`匯入失敗(狀態碼 ${resp.status})。${
msg ? `\n伺服器訊息:${msg.slice(0, 120)}` : ""
}`
);
alert(t("importFailStatus", {
status: resp.status,
detail: msg ? t("serverMessage", { message: msg.slice(0, 120) }) : "",
}));
return;
return;
}
}
@@ -453,10 +463,10 @@ export default function ProductionSchedulePage() {
// Backend clears item_fake_onhand then inserts payload rows,
// Backend clears item_fake_onhand then inserts payload rows,
// so after success the table should exactly match the uploaded Excel.
// so after success the table should exactly match the uploaded Excel.
await fetchItemDailyOut(true);
await fetchItemDailyOut(true);
alert(`已成功匯入並更新 ${result.count ?? payload.length} 筆排期庫存 (item_fake_onhand)。` );
alert(t("importOk", { count: result.count ?? payload.length }) );
} catch (e) {
} catch (e) {
console.error("Import fake on hand error:", e);
console.error("Import fake on hand error:", e);
alert("匯入失敗,請檢查檔案格式。" );
alert(t("importFailFormat") );
} finally {
} finally {
setIsImportingFakeOnHand(false);
setIsImportingFakeOnHand(false);
}
}
@@ -481,18 +491,18 @@ export default function ProductionSchedulePage() {
)
)
);
);
} else {
} else {
alert("儲存失敗" );
alert(t("saveFail") );
}
}
} catch (e) {
} catch (e) {
console.error("setDailyQtyOut Error:", e);
console.error("setDailyQtyOut Error:", e);
alert("儲存失敗" );
alert(t("saveFail") );
} finally {
} finally {
setDailyOutSavingCode(null);
setDailyOutSavingCode(null);
}
}
};
};
const handleClearDailyQty = async (itemCode: string) => {
const handleClearDailyQty = async (itemCode: string) => {
if (!confirm(`確定要清除${itemCode}的設定排期每天出貨量嗎?` )) return;
if (!confirm(t("confirmClearDailyOut", { itemCode }) )) return;
setDailyOutClearingCode(itemCode);
setDailyOutClearingCode(itemCode);
try {
try {
const response = await clientAuthFetch(
const response = await clientAuthFetch(
@@ -511,11 +521,11 @@ export default function ProductionSchedulePage() {
)
)
);
);
} else {
} else {
alert("清除失敗" );
alert(t("clearFail") );
}
}
} catch (e) {
} catch (e) {
console.error("clearDailyQtyOut Error:", e);
console.error("clearDailyQtyOut Error:", e);
alert("清除失敗" );
alert(t("clearFail") );
} finally {
} finally {
setDailyOutClearingCode(null);
setDailyOutClearingCode(null);
}
}
@@ -550,11 +560,11 @@ export default function ProductionSchedulePage() {
})
})
);
);
} else {
} else {
alert("設定失敗" );
alert(t("updateFail") );
}
}
} catch (e) {
} catch (e) {
console.error("setCoffeeOrTea Error:", e);
console.error("setCoffeeOrTea Error:", e);
alert("設定失敗" );
alert(t("updateFail") );
} finally {
} finally {
setCoffeeOrTeaUpdating(null);
setCoffeeOrTeaUpdating(null);
}
}
@@ -579,18 +589,18 @@ export default function ProductionSchedulePage() {
)
)
);
);
} else {
} else {
alert("設定失敗" );
alert(t("updateFail") );
}
}
} catch (e) {
} catch (e) {
console.error("setFakeOnHand Error:", e);
console.error("setFakeOnHand Error:", e);
alert("設定失敗" );
alert(t("updateFail") );
} finally {
} finally {
setFakeOnHandSavingCode(null);
setFakeOnHandSavingCode(null);
}
}
};
};
const handleClearFakeOnHand = async (itemCode: string) => {
const handleClearFakeOnHand = async (itemCode: string) => {
if (!confirm("確定要清除此物料的設定排期庫存嗎?" )) return;
if (!confirm(t("confirmClearScheduleStock") )) return;
setFakeOnHandClearingCode(itemCode);
setFakeOnHandClearingCode(itemCode);
try {
try {
const response = await clientAuthFetch(
const response = await clientAuthFetch(
@@ -609,11 +619,11 @@ export default function ProductionSchedulePage() {
)
)
);
);
} else {
} else {
alert("清除失敗" );
alert(t("clearFail") );
}
}
} catch (e) {
} catch (e) {
console.error("clearFakeOnHand Error:", e);
console.error("clearFakeOnHand Error:", e);
alert("清除失敗" );
alert(t("clearFail") );
} finally {
} finally {
setFakeOnHandClearingCode(null);
setFakeOnHandClearingCode(null);
}
}
@@ -622,7 +632,7 @@ export default function ProductionSchedulePage() {
return (
return (
<div className="space-y-4">
<div className="space-y-4">
<PageTitleBar
<PageTitleBar
title="生產排程"
title={t("pageTitle")}
actions={
actions={
<>
<>
<button
<button
@@ -631,7 +641,7 @@ export default function ProductionSchedulePage() {
className="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700"
className="inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700"
>
>
<Settings sx={{ fontSize: 16 }} />
<Settings sx={{ fontSize: 16 }} />
排期設定
{t("scheduleSettings")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -639,7 +649,7 @@ export default function ProductionSchedulePage() {
className="inline-flex items-center gap-2 rounded-lg border border-emerald-500/70 bg-white px-4 py-2 text-sm font-semibold text-emerald-600 shadow-sm transition hover:bg-emerald-50 dark:border-emerald-500/50 dark:bg-slate-800 dark:text-emerald-400 dark:hover:bg-emerald-500/10"
className="inline-flex items-center gap-2 rounded-lg border border-emerald-500/70 bg-white px-4 py-2 text-sm font-semibold text-emerald-600 shadow-sm transition hover:bg-emerald-50 dark:border-emerald-500/50 dark:bg-slate-800 dark:text-emerald-400 dark:hover:bg-emerald-500/10"
>
>
<Download sx={{ fontSize: 16 }} />
<Download sx={{ fontSize: 16 }} />
匯出計劃/物料需求Excel
{t("exportPlan")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -652,7 +662,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<Download sx={{ fontSize: 16 }} />
<Download sx={{ fontSize: 16 }} />
)}
)}
匯出送貨單數量
{t("exportDoQty")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -665,7 +675,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<ShowChart sx={{ fontSize: 16 }} />
<ShowChart sx={{ fontSize: 16 }} />
)}
)}
預測排期
{t("forecast")}
</button>
</button>
</>
</>
}
}
@@ -675,7 +685,7 @@ export default function ProductionSchedulePage() {
{/* Query Bar */}
{/* Query Bar */}
<div className="app-search-criteria mb-4 flex flex-wrap items-center gap-2 p-4">
<div className="app-search-criteria mb-4 flex flex-wrap items-center gap-2 p-4">
<label className="sr-only" htmlFor="ps-search-date">
<label className="sr-only" htmlFor="ps-search-date">
生產日期
{t("productionDate")}
</label>
</label>
<input
<input
id="ps-search-date"
id="ps-search-date"
@@ -690,7 +700,7 @@ export default function ProductionSchedulePage() {
className="inline-flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-600"
className="inline-flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-600"
>
>
<Search sx={{ fontSize: 16 }} />
<Search sx={{ fontSize: 16 }} />
搜索
{t("search")}
</button>
</button>
</div>
</div>
@@ -701,16 +711,16 @@ export default function ProductionSchedulePage() {
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<tr>
<tr>
<th className="w-[100px] px-4 py-3 text-center font-bold text-slate-700 dark:text-slate-200">
<th className="w-[100px] px-4 py-3 text-center font-bold text-slate-700 dark:text-slate-200">
詳細
{t("detail")}
</th>
</th>
<th className="px-4 py-3 font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-3 font-bold text-slate-700 dark:text-slate-200">
生產日期
{t("productionDate")}
</th>
</th>
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
預計生產數
{t("estProdQty")}
</th>
</th>
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
成品款數
{t("fgCount")}
</th>
</th>
</tr>
</tr>
</thead>
</thead>
@@ -761,8 +771,10 @@ export default function ProductionSchedulePage() {
<div className="flex items-center gap-2 border-b border-slate-200 bg-blue-500 px-4 py-3 text-white dark:border-slate-700">
<div className="flex items-center gap-2 border-b border-slate-200 bg-blue-500 px-4 py-3 text-white dark:border-slate-700">
<FormatListNumbered sx={{ fontSize: 20, flexShrink: 0 }} />
<FormatListNumbered sx={{ fontSize: 20, flexShrink: 0 }} />
<h2 id="detail-title" className="text-lg font-semibold">
<h2 id="detail-title" className="text-lg font-semibold">
排期詳細: {selectedPs?.id} (
{formatBackendDate(selectedPs?.produceAt)})
{t("detailTitle", {
id: selectedPs?.id,
date: formatBackendDate(selectedPs?.produceAt),
})}
</h2>
</h2>
</div>
</div>
<div className="max-h-[65vh] overflow-auto">
<div className="max-h-[65vh] overflow-auto">
@@ -770,34 +782,34 @@ export default function ProductionSchedulePage() {
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<tr>
<tr>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
工單號
{t("jobOrder")}
</th>
</th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
物料編號
{t("itemCode")}
</th>
</th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
物料名稱
{t("itemName")}
</th>
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
每日平均出貨量
{t("avgDailyOut")}
</th>
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
出貨前預計存貨量
{t("stockBeforeOut")}
</th>
</th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
單位
{t("unit")}
</th>
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
可用日
{t("daysOfCover")}
</th>
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
生產量(批)
{t("batchQty")}
</th>
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
預計生產包數
{t("estPacks")}
</th>
</th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">
優先度
{t("priority")}
</th>
</th>
</tr>
</tr>
</thead>
</thead>
@@ -856,7 +868,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<Hub sx={{ fontSize: 16 }} />
<Hub sx={{ fontSize: 16 }} />
)}
)}
自動生成工單
{t("generateJobs")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -864,7 +876,7 @@ export default function ProductionSchedulePage() {
disabled={isGenerating}
disabled={isGenerating}
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
>
>
關閉
{t("close")}
</button>
</button>
</div>
</div>
</div>
</div>
@@ -884,7 +896,7 @@ export default function ProductionSchedulePage() {
/>
/>
<div className="relative z-10 w-full max-w-sm rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800 sm:max-w-md">
<div className="relative z-10 w-full max-w-sm rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800 sm:max-w-md">
<h3 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">
<h3 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">
準備生成預計排期
{t("forecastDialogTitle")}
</h3>
</h3>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div>
<div>
@@ -892,7 +904,7 @@ export default function ProductionSchedulePage() {
htmlFor="forecast-start"
htmlFor="forecast-start"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
>
開始日期
{t("startDate")}
</label>
</label>
<input
<input
id="forecast-start"
id="forecast-start"
@@ -908,7 +920,7 @@ export default function ProductionSchedulePage() {
htmlFor="forecast-days"
htmlFor="forecast-days"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
>
排期日數
{t("scheduleDays")}
</label>
</label>
<input
<input
id="forecast-days"
id="forecast-days"
@@ -936,7 +948,7 @@ export default function ProductionSchedulePage() {
onClick={() => setIsForecastDialogOpen(false)}
onClick={() => setIsForecastDialogOpen(false)}
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
>
>
取消
{t("cancel")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -951,7 +963,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<ShowChart sx={{ fontSize: 16 }} />
<ShowChart sx={{ fontSize: 16 }} />
)}
)}
計算預測排期
{t("calculateForecast")}
</button>
</button>
</div>
</div>
</div>
</div>
@@ -971,10 +983,10 @@ export default function ProductionSchedulePage() {
/>
/>
<div className="relative z-10 w-full max-w-sm rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800">
<div className="relative z-10 w-full max-w-sm rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
匯出送貨單數量
{t("exportDoQty")}
</h3>
</h3>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
選擇日期範圍(僅含具 BOM 的物料,數量為庫存單位)
{t("exportDoQtyHint")}
</p>
</p>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div>
<div>
@@ -982,7 +994,7 @@ export default function ProductionSchedulePage() {
htmlFor="do-qty-export-from"
htmlFor="do-qty-export-from"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
>
開始日期
{t("startDate")}
</label>
</label>
<input
<input
id="do-qty-export-from"
id="do-qty-export-from"
@@ -997,7 +1009,7 @@ export default function ProductionSchedulePage() {
htmlFor="do-qty-export-to"
htmlFor="do-qty-export-to"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
>
結束日期
{t("endDate")}
</label>
</label>
<input
<input
id="do-qty-export-to"
id="do-qty-export-to"
@@ -1015,7 +1027,7 @@ export default function ProductionSchedulePage() {
disabled={isDoQtyExporting}
disabled={isDoQtyExporting}
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
>
>
取消
{t("cancel")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -1032,7 +1044,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<Download sx={{ fontSize: 16 }} />
<Download sx={{ fontSize: 16 }} />
)}
)}
匯出
{t("export")}
</button>
</button>
</div>
</div>
</div>
</div>
@@ -1052,16 +1064,16 @@ export default function ProductionSchedulePage() {
/>
/>
<div className="relative z-10 w-full max-w-xs rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800">
<div className="relative z-10 w-full max-w-xs rounded-lg border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-700 dark:bg-slate-800">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
匯出排期/物料用量預計
{t("exportPlanDialogTitle")}
</h3>
</h3>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
選擇要匯出的起始日期
{t("exportPlanHint")}
</p>
</p>
<label
<label
htmlFor="export-from"
htmlFor="export-from"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
>
起始日期
{t("fromDate")}
</label>
</label>
<input
<input
id="export-from"
id="export-from"
@@ -1077,7 +1089,7 @@ export default function ProductionSchedulePage() {
onClick={() => setIsExportDialogOpen(false)}
onClick={() => setIsExportDialogOpen(false)}
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
className="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600"
>
>
取消
{t("cancel")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -1090,7 +1102,7 @@ export default function ProductionSchedulePage() {
) : (
) : (
<Download sx={{ fontSize: 16 }} />
<Download sx={{ fontSize: 16 }} />
)}
)}
匯出
{t("export")}
</button>
</button>
</div>
</div>
</div>
</div>
@@ -1115,7 +1127,7 @@ export default function ProductionSchedulePage() {
id="settings-panel-title"
id="settings-panel-title"
className="text-lg font-semibold text-slate-900 dark:text-white"
className="text-lg font-semibold text-slate-900 dark:text-white"
>
>
排期設定
{t("scheduleSettings")}
</h2>
</h2>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
{settingsTab === "items" && (
{settingsTab === "items" && (
@@ -1131,18 +1143,18 @@ export default function ProductionSchedulePage() {
) : (
) : (
<Refresh sx={{ fontSize: 14 }} />
<Refresh sx={{ fontSize: 14 }} />
)}
)}
刷新系統庫存
{t("refreshStock")}
</button>
</button>
<button
<button
type="button"
type="button"
onClick={handleExportFakeOnHand}
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"
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
{t("exportScheduleStock")}
</button>
</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">
<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="inline-flex h-2 w-2 rounded-full bg-amber-500" />
<span className="text-amber-700 dark:text-amber-300">匯入排期庫存 Excel(覆蓋設定) </span>
<span className="text-amber-700 dark:text-amber-300">{t("importScheduleStock")} </span>
<input
<input
type="file"
type="file"
accept=".xlsx,.xls"
accept=".xlsx,.xls"
@@ -1163,7 +1175,7 @@ export default function ProductionSchedulePage() {
onClick={() => setIsDailyOutPanelOpen(false)}
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"
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"
>
>
關閉
{t("close")}
</button>
</button>
</div>
</div>
</div>
</div>
@@ -1177,7 +1189,7 @@ export default function ProductionSchedulePage() {
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
}`}
}`}
>
>
物料設定
{t("tabItems")}
</button>
</button>
<button
<button
type="button"
type="button"
@@ -1188,12 +1200,12 @@ export default function ProductionSchedulePage() {
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
}`}
}`}
>
>
排期規則
{t("tabRules")}
</button>
</button>
</div>
</div>
{settingsTab === "items" && (
{settingsTab === "items" && (
<p className="px-4 py-2 text-sm text-slate-600 dark:text-slate-400">
<p className="px-4 py-2 text-sm text-slate-600 dark:text-slate-400">
預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。
{t("itemsHint")}
</p>
</p>
)}
)}
{settingsTab === "rules" ? (
{settingsTab === "rules" ? (
@@ -1215,16 +1227,16 @@ export default function ProductionSchedulePage() {
<table className="w-full min-w-[900px] text-left text-sm">
<table className="w-full min-w-[900px] text-left text-sm">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<tr>
<tr>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">物料編號 </th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">物料名稱 </th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">單位 </th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">庫存 </th>
<th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期庫存 </th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">過去平均出貨量 </th>
<th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">設定排期每天出貨量 </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 A </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 B </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">類別 C </th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">{t("itemCode")} </th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">{t("itemName")} </th>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">{t("unit")} </th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">{t("onHand")} </th>
<th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">{t("scheduleStock")} </th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">{t("pastAvgOut")} </th>
<th className="px-4 py-2 text-left font-bold text-slate-700 dark:text-slate-200">{t("dailyOutOverride")} </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">{t("categoryA")} </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">{t("categoryB")} </th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">{t("categoryC")} </th>
</tr>
</tr>
</thead>
</thead>
<tbody>
<tbody>
@@ -1284,6 +1296,7 @@ function DailyOutRow({
fakeOnHandClearing: boolean;
fakeOnHandClearing: boolean;
formatNum: (n: any) => string;
formatNum: (n: any) => string;
}) {
}) {
const { t } = useTranslation("scheduling");
const [editQty, setEditQty] = useState<string>(
const [editQty, setEditQty] = useState<string>(
row.dailyQty != null ? String(row.dailyQty) : ""
row.dailyQty != null ? String(row.dailyQty) : ""
);
);
@@ -1335,7 +1348,7 @@ function DailyOutRow({
type="button"
type="button"
disabled={fakeOnHandClearing}
disabled={fakeOnHandClearing}
onClick={() => onClearFakeOnHand(row.itemCode)}
onClick={() => onClearFakeOnHand(row.itemCode)}
title="清除設定排期庫存"
title={t("clearScheduleStock")}
className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200"
className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200"
>
>
{fakeOnHandClearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}
{fakeOnHandClearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}
@@ -1365,7 +1378,7 @@ function DailyOutRow({
type="button"
type="button"
disabled={clearing}
disabled={clearing}
onClick={() => onClear(row.itemCode)}
onClick={() => onClear(row.itemCode)}
title="清除設定排期每天出貨量"
title={t("clearDailyOut")}
className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200"
className="rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600 disabled:opacity-50 dark:hover:bg-slate-600 dark:hover:text-slate-200"
>
>
{clearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}
{clearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}