瀏覽代碼

EN support for scheduling page

debrand
Harry Groves 3 週之前
父節點
當前提交
b6f41ecd3c
共有 4 個文件被更改,包括 426 次插入 和 162 次删除
  1. +67
    -56
      src/app/(main)/ps/PsRulesTab.tsx
  2. +117
    -104
      src/app/(main)/ps/page.tsx
  3. +121
    -1
      src/i18n/en/scheduling.json
  4. +121
    -1
      src/i18n/zh/scheduling.json

+ 67
- 56
src/app/(main)/ps/PsRulesTab.tsx 查看文件

@@ -3,17 +3,18 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { CircularProgress } from "@mui/material";
import Clear from "@mui/icons-material/Clear";
import { useTranslation } from "react-i18next";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";

const WEEKDAYS = [
{ id: "MON", label: "一" },
{ id: "TUE", label: "二" },
{ id: "WED", label: "三" },
{ id: "THU", label: "四" },
{ id: "FRI", label: "五" },
{ id: "SAT", label: "六" },
{ id: "SUN", label: "日" },
{ id: "MON", labelKey: "weekdayMon" },
{ id: "TUE", labelKey: "weekdayTue" },
{ id: "WED", labelKey: "weekdayWed" },
{ id: "THU", labelKey: "weekdayThu" },
{ id: "FRI", labelKey: "weekdayFri" },
{ id: "SAT", labelKey: "weekdaySat" },
{ id: "SUN", labelKey: "weekdaySun" },
] as const;

type WeekdayId = (typeof WEEKDAYS)[number]["id"];
@@ -74,6 +75,7 @@ function numText(value: unknown, fallback: string): string {
}

export default function PsRulesTab({ items }: { items: ItemOption[] }) {
const { t } = useTranslation("scheduling");
const [plant, setPlant] = useState<PlantRules>(emptyPlant);
const [overrides, setOverrides] = useState<Record<string, ItemOverride>>({});
const [loading, setLoading] = useState(true);
@@ -91,7 +93,7 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
);
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
alert("讀取排期規則失敗");
alert(t("loadRulesFail"));
return;
}
const data = await response.json();
@@ -138,7 +140,7 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
setOverrides(next);
} catch (e) {
console.error("schedule-rules Error:", e);
alert("讀取排期規則失敗");
alert(t("loadRulesFail"));
} finally {
setLoading(false);
}
@@ -155,15 +157,15 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
const prepRed = Number(plant.prepareMoreRedLineDays);
const prepSafety = Number(plant.prepareMoreSafetyStockDays);
if (![red, safety, prepRed, prepSafety].every((n) => Number.isFinite(n) && n > 0)) {
alert("可用日必須是大於 0 的數字。");
alert(t("daysMustBePositive"));
return;
}
if (red > safety || prepRed > prepSafety) {
alert("觸發日不可大於補貨日。");
alert(t("triggerNotAboveReplenish"));
return;
}
if (!plant.coffeeDays.length || !plant.teaDays.length || !plant.lemonDays.length) {
alert("類別 A / B / C 至少要各選一天可生產。");
alert(t("categoryNeedsDay"));
return;
}
plantLock.current = true;
@@ -186,13 +188,13 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
const body = await response.json().catch(() => ({}));
alert(body.error || "儲存失敗");
alert(body.error || t("saveFail"));
return;
}
alert("已儲存。下次按「預測排期」會用新規則。");
alert(t("rulesSaved"));
} catch (e) {
console.error("save schedule-rules Error:", e);
alert("儲存失敗");
alert(t("saveFail"));
} finally {
setSavingPlant(false);
plantLock.current = false;
@@ -222,21 +224,21 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
return (
<div className="space-y-4 p-4">
<p className="text-sm text-slate-600 dark:text-slate-400">
可用日 = 存貨 ÷ 每日平均出貨量。低於觸發日就安排生產,並補到補貨日。留空的物料跟隨這裡的預設。已產生的排期要再按「預測排期」才會更新。
{t("rulesIntro")}
</p>

<section className="rounded-lg border border-slate-200 p-4 dark:border-slate-700">
<h3 className="mb-3 text-sm font-semibold text-slate-900 dark:text-white">預設規則</h3>
<h3 className="mb-3 text-sm font-semibold text-slate-900 dark:text-white">{t("defaultRules")}</h3>
<div className="grid gap-4 md:grid-cols-2">
<DayPair
title="一般日"
title={t("normalDays")}
red={plant.redLineDays}
safety={plant.safetyStockDays}
onRed={(value) => setPlant((prev) => ({ ...prev, redLineDays: value }))}
onSafety={(value) => setPlant((prev) => ({ ...prev, safetyStockDays: value }))}
/>
<DayPair
title="多備日"
title={t("extraDays")}
red={plant.prepareMoreRedLineDays}
safety={plant.prepareMoreSafetyStockDays}
onRed={(value) => setPlant((prev) => ({ ...prev, prepareMoreRedLineDays: value }))}
@@ -247,33 +249,33 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
</div>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<DayChecks
label="多備星期"
label={t("extraWeekdays")}
selected={plant.prepareMoreDays}
onChange={(days) => setPlant((prev) => ({ ...prev, prepareMoreDays: days }))}
/>
<DayChecks
label="未分類物料不生產"
label={t("noProductionUncategorised")}
selected={plant.noProductionDays}
onChange={(days) => setPlant((prev) => ({ ...prev, noProductionDays: days }))}
/>
</div>
<div className="mt-4 space-y-3">
<CategoryDays
label="類別 A"
label={t("categoryA")}
days={plant.coffeeDays}
skip={plant.coffeeSkipPrepareMore}
onDays={(days) => setPlant((prev) => ({ ...prev, coffeeDays: days }))}
onSkip={(skip) => setPlant((prev) => ({ ...prev, coffeeSkipPrepareMore: skip }))}
/>
<CategoryDays
label="類別 B"
label={t("categoryB")}
days={plant.teaDays}
skip={plant.teaSkipPrepareMore}
onDays={(days) => setPlant((prev) => ({ ...prev, teaDays: days }))}
onSkip={(skip) => setPlant((prev) => ({ ...prev, teaSkipPrepareMore: skip }))}
/>
<CategoryDays
label="類別 C"
label={t("categoryC")}
days={plant.lemonDays}
skip={plant.lemonSkipPrepareMore}
onDays={(days) => setPlant((prev) => ({ ...prev, lemonDays: days }))}
@@ -288,18 +290,18 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
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 disabled:opacity-50"
>
{savingPlant && <CircularProgress size={14} sx={{ display: "block" }} />}
儲存預設
{t("saveDefaults")}
</button>
</div>
</section>

<section className="rounded-lg border border-slate-200 dark:border-slate-700">
<div className="flex flex-wrap items-center gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-700">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">個別物料</h3>
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{t("perItem")}</h3>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="搜尋物料編號或名稱"
placeholder={t("searchItem")}
className="w-56 rounded border border-slate-300 bg-white px-2 py-1 text-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100"
/>
<label className="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
@@ -308,19 +310,19 @@ export default function PsRulesTab({ items }: { items: ItemOption[] }) {
checked={onlyOverrides}
onChange={(e) => setOnlyOverrides(e.target.checked)}
/>
只顯示已自訂
{t("customOnly")}
</label>
</div>
<div className="max-h-[42vh] overflow-auto">
<table className="w-full min-w-[860px] text-left text-sm">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<tr>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">物料編號</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">物料名稱</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">觸發日</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">補貨日</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">可生產日</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">多備</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("itemCode")}</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("itemName")}</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("triggerDays")}</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("replenishDays")}</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("productionDays")}</th>
<th className="px-3 py-2 font-bold text-slate-700 dark:text-slate-200">{t("extraStock")}</th>
<th className="px-3 py-2" />
</tr>
</thead>
@@ -361,15 +363,21 @@ function DayPair({
onRed: (value: string) => void;
onSafety: (value: string) => void;
}) {
const { t } = useTranslation("scheduling");
return (
<div>
<div className="mb-2 text-sm font-medium text-slate-800 dark:text-slate-100">{title}</div>
<div className="flex flex-wrap items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<span>低於</span>
<NumInput value={red} onChange={onRed} />
<span>日生產,補至</span>
<NumInput value={safety} onChange={onSafety} />
<span>日</span>
<div className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
<div className="flex items-center gap-2 whitespace-nowrap">
<span>{t("below")}</span>
<NumInput value={red} onChange={onRed} />
<span>{t("produceUpTo")}</span>
</div>
<div className="flex items-center gap-2 whitespace-nowrap">
<span>{t("topUpTo")}</span>
<NumInput value={safety} onChange={onSafety} />
<span>{t("days")}</span>
</div>
</div>
</div>
);
@@ -399,6 +407,7 @@ function DayChecks({
onChange: (days: WeekdayId[]) => void;
disabled?: boolean;
}) {
const { t } = useTranslation("scheduling");
return (
<div>
{label ? (
@@ -417,13 +426,13 @@ function DayChecks({
on ? selected.filter((id) => id !== day.id) : [...selected, day.id]
)
}
className={`h-8 w-8 rounded border text-sm ${
className={`h-8 min-w-8 rounded border px-1.5 text-xs ${
on
? "border-blue-500 bg-blue-500 text-white"
: "border-slate-300 bg-white text-slate-600 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300"
} disabled:opacity-40`}
>
{day.label}
{t(day.labelKey)}
</button>
);
})}
@@ -445,13 +454,14 @@ function CategoryDays({
onDays: (days: WeekdayId[]) => void;
onSkip: (skip: boolean) => void;
}) {
const { t } = useTranslation("scheduling");
return (
<div className="flex flex-wrap items-center gap-3">
<div className="w-16 text-sm font-medium text-slate-800 dark:text-slate-100">{label}</div>
<div className="w-24 shrink-0 text-sm font-medium text-slate-800 dark:text-slate-100">{label}</div>
<DayChecks label="" selected={days} onChange={onDays} />
<label className="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<input type="checkbox" checked={skip} onChange={(e) => onSkip(e.target.checked)} />
不套用多備
{t("skipExtra")}
</label>
</div>
);
@@ -474,6 +484,7 @@ function ItemRuleRow({
produceDays: [],
skipPrepareMore: null,
});
const { t } = useTranslation("scheduling");
const [draft, setDraft] = useState<ItemOverride>(value ?? blank());
const [saving, setSaving] = useState(false);
const lock = useRef(false);
@@ -491,15 +502,15 @@ function ItemRuleRow({
const red = next.redLineDays.trim() === "" ? null : Number(next.redLineDays);
const safety = next.safetyStockDays.trim() === "" ? null : Number(next.safetyStockDays);
if ((red != null && (!Number.isFinite(red) || red <= 0)) || (safety != null && (!Number.isFinite(safety) || safety <= 0))) {
alert("可用日必須是大於 0 的數字,或留空跟隨預設。");
alert(t("daysBlankFollowDefault"));
return;
}
if (red != null && safety != null && red > safety) {
alert("觸發日不可大於補貨日。");
alert(t("triggerNotAboveReplenish"));
return;
}
if (next.useCustomProduceDays && next.produceDays.length === 0) {
alert("自訂可生產日至少要選一天。");
alert(t("customDaysRequired"));
return;
}
const empty =
@@ -522,13 +533,13 @@ function ItemRuleRow({
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
const body = await response.json().catch(() => ({}));
alert(body.error || "儲存失敗");
alert(body.error || t("saveFail"));
return;
}
onSaved(empty ? null : next);
} catch (e) {
console.error("setItemPsRule Error:", e);
alert("儲存失敗");
alert(t("saveFail"));
} finally {
setSaving(false);
lock.current = false;
@@ -547,7 +558,7 @@ function ItemRuleRow({
type="number"
min={0}
step={0.1}
placeholder="預設"
placeholder={t("defaultPlaceholder")}
value={draft.redLineDays}
onChange={(e) => setDraft((prev) => ({ ...prev, redLineDays: e.target.value }))}
onBlur={() => void persist(draft)}
@@ -559,7 +570,7 @@ function ItemRuleRow({
type="number"
min={0}
step={0.1}
placeholder="預設"
placeholder={t("defaultPlaceholder")}
value={draft.safetyStockDays}
onChange={(e) => setDraft((prev) => ({ ...prev, safetyStockDays: e.target.value }))}
onBlur={() => void persist(draft)}
@@ -581,7 +592,7 @@ function ItemRuleRow({
if (!e.target.checked) void persist(next);
}}
/>
自訂
{t("custom")}
</label>
<DayChecks
label=""
@@ -606,9 +617,9 @@ function ItemRuleRow({
}}
className="rounded border border-slate-300 bg-white px-2 py-1 text-sm dark:border-slate-600 dark:bg-slate-700"
>
<option value="">跟隨預設</option>
<option value="apply">套用多備</option>
<option value="skip">不套用多備</option>
<option value="">{t("followDefault")}</option>
<option value="apply">{t("applyExtra")}</option>
<option value="skip">{t("skipExtra")}</option>
</select>
</td>
<td className="px-3 py-2">
@@ -617,7 +628,7 @@ function ItemRuleRow({
) : value ? (
<button
type="button"
title="清除此物料的自訂規則"
title={t("clearItemRule")}
onClick={() => {
const next = blank();
setDraft(next);


+ 117
- 104
src/app/(main)/ps/page.tsx 查看文件

@@ -11,6 +11,7 @@ import Settings from "@mui/icons-material/Settings";
import Clear from "@mui/icons-material/Clear";
import Refresh from "@mui/icons-material/Refresh";
import { CircularProgress } from "@mui/material";
import { useTranslation } from "react-i18next";
import PageTitleBar from "@/components/PageTitleBar";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
@@ -33,6 +34,7 @@ type ItemDailyOutRow = {
};

export default function ProductionSchedulePage() {
const { t } = useTranslation("scheduling");
const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD"));
const [schedules, setSchedules] = useState<any[]>([]);
const [selectedLines, setSelectedLines] = useState<any[]>([]);
@@ -111,7 +113,7 @@ export default function ProductionSchedulePage() {

const handleConfirmForecast = async () => {
if (!forecastStartDate || forecastDays === "" || forecastDays < 1) {
alert("Please enter a valid start date and number of days (≥1).");
alert(t("forecastInvalid"));
return;
}
setLoading(true);
@@ -126,15 +128,15 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.ok) {
await handleSearch();
alert("成功計算排期!");
alert(t("forecastOk"));
} else {
const errorText = await response.text();
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) {
console.error("Forecast Error:", e);
alert("發生不明狀況.");
alert(t("forecastUnknown"));
} finally {
setLoading(false);
}
@@ -143,11 +145,11 @@ export default function ProductionSchedulePage() {
const handleConfirmDoQtyExport = async () => {
if (doQtyExportInFlightRef.current) return;
if (!doQtyExportFromDate || !doQtyExportToDate) {
alert("請選擇開始及結束日期。");
alert(t("doQtyDatesRequired"));
return;
}
if (dayjs(doQtyExportFromDate).isAfter(dayjs(doQtyExportToDate))) {
alert("開始日期不可晚於結束日期。");
alert(t("doQtyDateOrder"));
return;
}
doQtyExportInFlightRef.current = true;
@@ -178,7 +180,7 @@ export default function ProductionSchedulePage() {
document.body.removeChild(a);
} catch (e) {
console.error("DO qty export Error:", e);
alert("匯出送貨單數量失敗。");
alert(t("doQtyExportFail"));
} finally {
setIsDoQtyExporting(false);
doQtyExportInFlightRef.current = false;
@@ -187,7 +189,7 @@ export default function ProductionSchedulePage() {

const handleConfirmExport = async () => {
if (!exportFromDate) {
alert("Please select a from date.");
alert(t("exportFromRequired"));
return;
}
setLoading(true);
@@ -211,7 +213,7 @@ export default function ProductionSchedulePage() {
document.body.removeChild(a);
} catch (e) {
console.error("Export Error:", e);
alert("Failed to export file.");
alert(t("exportFail"));
} finally {
setLoading(false);
}
@@ -219,7 +221,7 @@ export default function ProductionSchedulePage() {

const handleViewDetail = async (ps: any) => {
if (!ps?.id) {
alert("Cannot open details: missing schedule ID");
alert(t("detailMissingId"));
return;
}
setSelectedPs(ps);
@@ -230,7 +232,7 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
const errorText = await response.text().catch(() => "(no text)");
alert(`Server error ${response.status}: ${errorText}`);
alert(t("serverError", { status: response.status, detail: errorText }));
return;
}
const data = await response.json();
@@ -238,7 +240,7 @@ export default function ProductionSchedulePage() {
setIsDetailOpen(true);
} catch (err) {
console.error("Fetch failed:", err);
alert("Network or fetch error – check console");
alert(t("networkError"));
} finally {
setLoading(false);
}
@@ -258,10 +260,15 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (response.ok) {
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);
} else {
alert("Failed to generate jobs.");
alert(t("generateJobsFail"));
}
} catch (e) {
console.error("Release Error:", e);
@@ -347,14 +354,17 @@ export default function ProductionSchedulePage() {
if (response.status === 401 || response.status === 403) return;
if (!response.ok) {
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;
}
await response.json().catch(() => null);
await fetchItemDailyOut(true);
} catch (e) {
console.error("refreshInventoryOnHand Error:", e);
alert("刷新庫存失敗。");
alert(t("refreshStockFail"));
} finally {
setIsRefreshingOnHand(false);
refreshOnHandInFlightRef.current = false;
@@ -381,13 +391,13 @@ export default function ProductionSchedulePage() {
const workbook = XLSX.read(data, { type: "array" });
const sheetName = workbook.SheetNames[0];
if (!sheetName) {
alert("Excel 沒有工作表。");
alert(t("excelNoSheet"));
return;
}
const sheet = workbook.Sheets[sheetName];
const rows: any[] = XLSX.utils.sheet_to_json(sheet, { defval: null });
if (!rows.length) {
alert("Excel 內容為空。");
alert(t("excelEmpty"));
return;
}

@@ -414,17 +424,18 @@ export default function ProductionSchedulePage() {
.filter((r): r is { itemCode: string; onHandQty: number | null } => r !== null);

if (!payload.length) {
alert("找不到任何有效的 itemCode。");
alert(t("excelNoItemCode"));
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 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`, {
@@ -434,17 +445,16 @@ export default function ProductionSchedulePage() {
});

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

if (!resp.ok) {
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;
}

@@ -453,10 +463,10 @@ export default function ProductionSchedulePage() {
// 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)。`);
alert(t("importOk", { count: result.count ?? payload.length }));
} catch (e) {
console.error("Import fake on hand error:", e);
alert("匯入失敗,請檢查檔案格式。");
alert(t("importFailFormat"));
} finally {
setIsImportingFakeOnHand(false);
}
@@ -481,18 +491,18 @@ export default function ProductionSchedulePage() {
)
);
} else {
alert("儲存失敗");
alert(t("saveFail"));
}
} catch (e) {
console.error("setDailyQtyOut Error:", e);
alert("儲存失敗");
alert(t("saveFail"));
} finally {
setDailyOutSavingCode(null);
}
};

const handleClearDailyQty = async (itemCode: string) => {
if (!confirm(`確定要清除${itemCode}的設定排期每天出貨量嗎?`)) return;
if (!confirm(t("confirmClearDailyOut", { itemCode }))) return;
setDailyOutClearingCode(itemCode);
try {
const response = await clientAuthFetch(
@@ -511,11 +521,11 @@ export default function ProductionSchedulePage() {
)
);
} else {
alert("清除失敗");
alert(t("clearFail"));
}
} catch (e) {
console.error("clearDailyQtyOut Error:", e);
alert("清除失敗");
alert(t("clearFail"));
} finally {
setDailyOutClearingCode(null);
}
@@ -550,11 +560,11 @@ export default function ProductionSchedulePage() {
})
);
} else {
alert("設定失敗");
alert(t("updateFail"));
}
} catch (e) {
console.error("setCoffeeOrTea Error:", e);
alert("設定失敗");
alert(t("updateFail"));
} finally {
setCoffeeOrTeaUpdating(null);
}
@@ -579,18 +589,18 @@ export default function ProductionSchedulePage() {
)
);
} else {
alert("設定失敗");
alert(t("updateFail"));
}
} catch (e) {
console.error("setFakeOnHand Error:", e);
alert("設定失敗");
alert(t("updateFail"));
} finally {
setFakeOnHandSavingCode(null);
}
};

const handleClearFakeOnHand = async (itemCode: string) => {
if (!confirm("確定要清除此物料的設定排期庫存嗎?")) return;
if (!confirm(t("confirmClearScheduleStock"))) return;
setFakeOnHandClearingCode(itemCode);
try {
const response = await clientAuthFetch(
@@ -609,11 +619,11 @@ export default function ProductionSchedulePage() {
)
);
} else {
alert("清除失敗");
alert(t("clearFail"));
}
} catch (e) {
console.error("clearFakeOnHand Error:", e);
alert("清除失敗");
alert(t("clearFail"));
} finally {
setFakeOnHandClearingCode(null);
}
@@ -622,7 +632,7 @@ export default function ProductionSchedulePage() {
return (
<div className="space-y-4">
<PageTitleBar
title="生產排程"
title={t("pageTitle")}
actions={
<>
<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"
>
<Settings sx={{ fontSize: 16 }} />
排期設定
{t("scheduleSettings")}
</button>
<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"
>
<Download sx={{ fontSize: 16 }} />
匯出計劃/物料需求Excel
{t("exportPlan")}
</button>
<button
type="button"
@@ -652,7 +662,7 @@ export default function ProductionSchedulePage() {
) : (
<Download sx={{ fontSize: 16 }} />
)}
匯出送貨單數量
{t("exportDoQty")}
</button>
<button
type="button"
@@ -665,7 +675,7 @@ export default function ProductionSchedulePage() {
) : (
<ShowChart sx={{ fontSize: 16 }} />
)}
預測排期
{t("forecast")}
</button>
</>
}
@@ -675,7 +685,7 @@ export default function ProductionSchedulePage() {
{/* Query Bar */}
<div className="app-search-criteria mb-4 flex flex-wrap items-center gap-2 p-4">
<label className="sr-only" htmlFor="ps-search-date">
生產日期
{t("productionDate")}
</label>
<input
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"
>
<Search sx={{ fontSize: 16 }} />
搜索
{t("search")}
</button>
</div>

@@ -701,16 +711,16 @@ export default function ProductionSchedulePage() {
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<tr>
<th className="w-[100px] px-4 py-3 text-center font-bold text-slate-700 dark:text-slate-200">
詳細
{t("detail")}
</th>
<th className="px-4 py-3 font-bold text-slate-700 dark:text-slate-200">
生產日期
{t("productionDate")}
</th>
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
預計生產數
{t("estProdQty")}
</th>
<th className="px-4 py-3 text-right font-bold text-slate-700 dark:text-slate-200">
成品款數
{t("fgCount")}
</th>
</tr>
</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">
<FormatListNumbered sx={{ fontSize: 20, flexShrink: 0 }} />
<h2 id="detail-title" className="text-lg font-semibold">
排期詳細: {selectedPs?.id} (
{formatBackendDate(selectedPs?.produceAt)})
{t("detailTitle", {
id: selectedPs?.id,
date: formatBackendDate(selectedPs?.produceAt),
})}
</h2>
</div>
<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">
<tr>
<th className="px-4 py-2 font-bold text-slate-700 dark:text-slate-200">
工單號
{t("jobOrder")}
</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 text-right font-bold text-slate-700 dark:text-slate-200">
每日平均出貨量
{t("avgDailyOut")}
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
出貨前預計存貨量
{t("stockBeforeOut")}
</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("daysOfCover")}
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
生產量(批)
{t("batchQty")}
</th>
<th className="px-4 py-2 text-right font-bold text-slate-700 dark:text-slate-200">
預計生產包數
{t("estPacks")}
</th>
<th className="px-4 py-2 text-center font-bold text-slate-700 dark:text-slate-200">
優先度
{t("priority")}
</th>
</tr>
</thead>
@@ -856,7 +868,7 @@ export default function ProductionSchedulePage() {
) : (
<Hub sx={{ fontSize: 16 }} />
)}
自動生成工單
{t("generateJobs")}
</button>
<button
type="button"
@@ -864,7 +876,7 @@ export default function ProductionSchedulePage() {
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"
>
關閉
{t("close")}
</button>
</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">
<h3 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white">
準備生成預計排期
{t("forecastDialogTitle")}
</h3>
<div className="flex flex-col gap-4">
<div>
@@ -892,7 +904,7 @@ export default function ProductionSchedulePage() {
htmlFor="forecast-start"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
開始日期
{t("startDate")}
</label>
<input
id="forecast-start"
@@ -908,7 +920,7 @@ export default function ProductionSchedulePage() {
htmlFor="forecast-days"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
排期日數
{t("scheduleDays")}
</label>
<input
id="forecast-days"
@@ -936,7 +948,7 @@ export default function ProductionSchedulePage() {
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"
>
取消
{t("cancel")}
</button>
<button
type="button"
@@ -951,7 +963,7 @@ export default function ProductionSchedulePage() {
) : (
<ShowChart sx={{ fontSize: 16 }} />
)}
計算預測排期
{t("calculateForecast")}
</button>
</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">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
匯出送貨單數量
{t("exportDoQty")}
</h3>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
選擇日期範圍(僅含具 BOM 的物料,數量為庫存單位)
{t("exportDoQtyHint")}
</p>
<div className="flex flex-col gap-4">
<div>
@@ -982,7 +994,7 @@ export default function ProductionSchedulePage() {
htmlFor="do-qty-export-from"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
開始日期
{t("startDate")}
</label>
<input
id="do-qty-export-from"
@@ -997,7 +1009,7 @@ export default function ProductionSchedulePage() {
htmlFor="do-qty-export-to"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
結束日期
{t("endDate")}
</label>
<input
id="do-qty-export-to"
@@ -1015,7 +1027,7 @@ export default function ProductionSchedulePage() {
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"
>
取消
{t("cancel")}
</button>
<button
type="button"
@@ -1032,7 +1044,7 @@ export default function ProductionSchedulePage() {
) : (
<Download sx={{ fontSize: 16 }} />
)}
匯出
{t("export")}
</button>
</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">
<h3 className="mb-2 text-lg font-semibold text-slate-900 dark:text-white">
匯出排期/物料用量預計
{t("exportPlanDialogTitle")}
</h3>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
選擇要匯出的起始日期
{t("exportPlanHint")}
</p>
<label
htmlFor="export-from"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
起始日期
{t("fromDate")}
</label>
<input
id="export-from"
@@ -1077,7 +1089,7 @@ export default function ProductionSchedulePage() {
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"
>
取消
{t("cancel")}
</button>
<button
type="button"
@@ -1090,7 +1102,7 @@ export default function ProductionSchedulePage() {
) : (
<Download sx={{ fontSize: 16 }} />
)}
匯出
{t("export")}
</button>
</div>
</div>
@@ -1115,7 +1127,7 @@ export default function ProductionSchedulePage() {
id="settings-panel-title"
className="text-lg font-semibold text-slate-900 dark:text-white"
>
排期設定
{t("scheduleSettings")}
</h2>
<div className="flex items-center gap-2">
{settingsTab === "items" && (
@@ -1131,18 +1143,18 @@ export default function ProductionSchedulePage() {
) : (
<Refresh sx={{ fontSize: 14 }} />
)}
刷新系統庫存
{t("refreshStock")}
</button>
<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
{t("exportScheduleStock")}
</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>
<span className="text-amber-700 dark:text-amber-300">{t("importScheduleStock")}</span>
<input
type="file"
accept=".xlsx,.xls"
@@ -1163,7 +1175,7 @@ export default function ProductionSchedulePage() {
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"
>
關閉
{t("close")}
</button>
</div>
</div>
@@ -1177,7 +1189,7 @@ export default function ProductionSchedulePage() {
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
}`}
>
物料設定
{t("tabItems")}
</button>
<button
type="button"
@@ -1188,12 +1200,12 @@ export default function ProductionSchedulePage() {
: "border-transparent text-slate-500 hover:text-slate-700 dark:text-slate-400"
}`}
>
排期規則
{t("tabRules")}
</button>
</div>
{settingsTab === "items" && (
<p className="px-4 py-2 text-sm text-slate-600 dark:text-slate-400">
預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。
{t("itemsHint")}
</p>
)}
{settingsTab === "rules" ? (
@@ -1215,16 +1227,16 @@ export default function ProductionSchedulePage() {
<table className="w-full min-w-[900px] text-left text-sm">
<thead className="sticky top-0 bg-slate-50 dark:bg-slate-700">
<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>
</thead>
<tbody>
@@ -1284,6 +1296,7 @@ function DailyOutRow({
fakeOnHandClearing: boolean;
formatNum: (n: any) => string;
}) {
const { t } = useTranslation("scheduling");
const [editQty, setEditQty] = useState<string>(
row.dailyQty != null ? String(row.dailyQty) : ""
);
@@ -1335,7 +1348,7 @@ function DailyOutRow({
type="button"
disabled={fakeOnHandClearing}
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"
>
{fakeOnHandClearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}
@@ -1365,7 +1378,7 @@ function DailyOutRow({
type="button"
disabled={clearing}
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"
>
{clearing ? <CircularProgress size={14} sx={{ display: "block" }} /> : <Clear sx={{ fontSize: 18 }} />}


+ 121
- 1
src/i18n/en/scheduling.json 查看文件

@@ -1,5 +1,125 @@
{
"title": "Scheduling",
"Scheduling": "Scheduling",
"ps": "Scheduling"
"ps": "Scheduling",
"pageTitle": "Production schedule",
"scheduleSettings": "Schedule settings",
"exportPlan": "Export plan / material demand",
"exportDoQty": "Export delivery qty",
"forecast": "Forecast schedule",
"productionDate": "Production date",
"search": "Search",
"detail": "Detail",
"estProdQty": "Planned qty",
"fgCount": "Finished goods",
"detailTitle": "Schedule {{id}} ({{date}})",
"jobOrder": "Job order",
"itemCode": "Item code",
"itemName": "Item name",
"avgDailyOut": "Avg daily outbound",
"stockBeforeOut": "Stock before outbound",
"unit": "Unit",
"daysOfCover": "Days of cover",
"batchQty": "Batches",
"estPacks": "Planned packs",
"priority": "Priority",
"generateJobs": "Generate job orders",
"close": "Close",
"forecastDialogTitle": "Generate forecast",
"startDate": "Start date",
"scheduleDays": "Days to schedule",
"cancel": "Cancel",
"calculateForecast": "Calculate forecast",
"exportDoQtyHint": "Choose a date range. Only items with a BOM are included, and quantity is in the stock unit.",
"endDate": "End date",
"export": "Export",
"exportPlanDialogTitle": "Export schedule and material usage",
"exportPlanHint": "Choose the start date to export.",
"fromDate": "From date",
"refreshStock": "Refresh stock",
"exportScheduleStock": "Export stock",
"importScheduleStock": "Import stock (overwrite)",
"tabItems": "Items",
"tabRules": "Rules",
"itemsHint": "Defaults to the past 30 days, including today. Schedule stock and daily outbound can be edited and are saved per row.",
"onHand": "On hand",
"scheduleStock": "Schedule stock",
"pastAvgOut": "Past avg outbound",
"dailyOutOverride": "Daily outbound override",
"categoryA": "Category A",
"categoryB": "Category B",
"categoryC": "Category C",
"clearScheduleStock": "Clear schedule stock",
"clearDailyOut": "Clear daily outbound override",
"forecastInvalid": "Enter a start date and a number of days (1 or more).",
"forecastOk": "Forecast calculated.",
"forecastError": "Calculation failed: {{status}} - {{detail}}",
"forecastUnknown": "Something went wrong.",
"doQtyDatesRequired": "Select a start date and an end date.",
"doQtyDateOrder": "The start date cannot be after the end date.",
"doQtyExportFail": "Could not export delivery quantities.",
"exportFromRequired": "Select a from date.",
"exportFail": "Could not export the file.",
"detailMissingId": "Cannot open this schedule because the schedule ID is missing.",
"serverError": "Server error {{status}}: {{detail}}",
"networkError": "Could not load the schedule. Check the connection and try again.",
"operationDone": "Done.",
"jobsCreated": "Created {{count}} job orders.",
"jobsNone": "No new job orders were created. These finished goods may already have a job in progress.",
"generateJobsFail": "Could not generate job orders.",
"refreshStockFailStatus": "Could not refresh stock ({{status}}){{detail}}",
"refreshStockFail": "Could not refresh stock.",
"excelNoSheet": "The Excel file has no worksheet.",
"excelEmpty": "The Excel file is empty.",
"excelNoItemCode": "No valid item codes were found.",
"importOutsideBom": "These item codes are outside the schedule BOM. The forecast will not use them; they will only be saved as stock overrides.\n\n{{preview}}",
"importOutsideBomMore": " ({{count}} in total)",
"sessionExpired": "Your session has expired, or you do not have permission. Sign in and try again.",
"importFailStatus": "Import failed (status {{status}}).{{detail}}",
"serverMessage": "\nServer message: {{message}}",
"importOk": "Imported {{count}} schedule stock rows.",
"importFailFormat": "Import failed. Check the file format.",
"saveFail": "Could not save.",
"confirmClearDailyOut": "Clear the daily outbound override for {{itemCode}}?",
"clearFail": "Could not clear.",
"updateFail": "Could not update.",
"confirmClearScheduleStock": "Clear the schedule stock override for this item?",
"loadRulesFail": "Could not load the schedule rules.",
"daysMustBePositive": "Days of cover must be a number greater than 0.",
"triggerNotAboveReplenish": "Trigger days cannot be greater than replenish days.",
"categoryNeedsDay": "Categories A, B, and C each need at least one production day.",
"rulesSaved": "Saved. Run Forecast schedule again to apply these rules.",
"rulesIntro": "Days of cover = stock ÷ average daily outbound. Production is scheduled when cover falls below the trigger, and stock is topped up to the replenish days. Items left blank follow these defaults. Schedules already created change only after you run Forecast schedule again.",
"defaultRules": "Default rules",
"normalDays": "Normal days",
"extraDays": "Extra-stock days",
"below": "Below",
"produceUpTo": "days",
"topUpTo": "Top up to",
"days": "days",
"extraWeekdays": "Extra-stock weekdays",
"noProductionUncategorised": "No production for unclassified items",
"skipExtra": "Skip extra stock",
"saveDefaults": "Save defaults",
"perItem": "Per item",
"searchItem": "Search item code or name",
"customOnly": "Custom rules only",
"triggerDays": "Trigger days",
"replenishDays": "Replenish days",
"productionDays": "Production days",
"extraStock": "Extra stock",
"daysBlankFollowDefault": "Days of cover must be greater than 0, or leave the field blank to follow the default.",
"customDaysRequired": "Choose at least one custom production day.",
"defaultPlaceholder": "Default",
"custom": "Custom",
"followDefault": "Follow default",
"applyExtra": "Apply extra stock",
"clearItemRule": "Clear this item's custom rule",
"weekdayMon": "Mon",
"weekdayTue": "Tue",
"weekdayWed": "Wed",
"weekdayThu": "Thu",
"weekdayFri": "Fri",
"weekdaySat": "Sat",
"weekdaySun": "Sun"
}

+ 121
- 1
src/i18n/zh/scheduling.json 查看文件

@@ -1,5 +1,125 @@
{
"title": "排程",
"Scheduling": "排程",
"ps": "排程"
"ps": "排程",
"pageTitle": "生產排程",
"scheduleSettings": "排期設定",
"exportPlan": "匯出計劃/物料需求Excel",
"exportDoQty": "匯出送貨單數量",
"forecast": "預測排期",
"productionDate": "生產日期",
"search": "搜索",
"detail": "詳細",
"estProdQty": "預計生產數",
"fgCount": "成品款數",
"detailTitle": "排期詳細: {{id}} ({{date}})",
"jobOrder": "工單號",
"itemCode": "物料編號",
"itemName": "物料名稱",
"avgDailyOut": "每日平均出貨量",
"stockBeforeOut": "出貨前預計存貨量",
"unit": "單位",
"daysOfCover": "可用日",
"batchQty": "生產量(批)",
"estPacks": "預計生產包數",
"priority": "優先度",
"generateJobs": "自動生成工單",
"close": "關閉",
"forecastDialogTitle": "準備生成預計排期",
"startDate": "開始日期",
"scheduleDays": "排期日數",
"cancel": "取消",
"calculateForecast": "計算預測排期",
"exportDoQtyHint": "選擇日期範圍(僅含具 BOM 的物料,數量為庫存單位)",
"endDate": "結束日期",
"export": "匯出",
"exportPlanDialogTitle": "匯出排期/物料用量預計",
"exportPlanHint": "選擇要匯出的起始日期",
"fromDate": "起始日期",
"refreshStock": "刷新系統庫存",
"exportScheduleStock": "匯出排期庫存 Excel",
"importScheduleStock": "匯入排期庫存 Excel(覆蓋設定)",
"tabItems": "物料設定",
"tabRules": "排期規則",
"itemsHint": "預設為過去 30 天(含今日)。設定排期每天出貨量、設定排期庫存可編輯並按列儲存。",
"onHand": "庫存",
"scheduleStock": "設定排期庫存",
"pastAvgOut": "過去平均出貨量",
"dailyOutOverride": "設定排期每天出貨量",
"categoryA": "類別 A",
"categoryB": "類別 B",
"categoryC": "類別 C",
"clearScheduleStock": "清除設定排期庫存",
"clearDailyOut": "清除設定排期每天出貨量",
"forecastInvalid": "請輸入有效的開始日期及日數(1 或以上)。",
"forecastOk": "成功計算排期!",
"forecastError": "計算錯誤: {{status}} - {{detail}}",
"forecastUnknown": "發生不明狀況.",
"doQtyDatesRequired": "請選擇開始及結束日期。",
"doQtyDateOrder": "開始日期不可晚於結束日期。",
"doQtyExportFail": "匯出送貨單數量失敗。",
"exportFromRequired": "請選擇起始日期。",
"exportFail": "匯出失敗。",
"detailMissingId": "無法開啟詳細資料:缺少排期編號。",
"serverError": "伺服器錯誤 {{status}}: {{detail}}",
"networkError": "無法載入排期,請檢查連線後再試。",
"operationDone": "操作完成。",
"jobsCreated": "成功建立 {{count}} 張工單!",
"jobsNone": "沒有建立新工單(可能因為成品已有生產中的工單).",
"generateJobsFail": "生成工單失敗。",
"refreshStockFailStatus": "刷新庫存失敗({{status}}){{detail}}",
"refreshStockFail": "刷新庫存失敗。",
"excelNoSheet": "Excel 沒有工作表。",
"excelEmpty": "Excel 內容為空。",
"excelNoItemCode": "找不到任何有效的物料編號。",
"importOutsideBom": "注意:以下物料編號不在排期 BOM 範圍內,預測不會受影響,只會寫入覆蓋表。\n\n{{preview}}",
"importOutsideBomMore": " 等共 {{count}} 筆",
"sessionExpired": "登入已過期或沒有權限,請重新登入後再試。",
"importFailStatus": "匯入失敗(狀態碼 {{status}})。{{detail}}",
"serverMessage": "\n伺服器訊息:{{message}}",
"importOk": "已成功匯入並更新 {{count}} 筆排期庫存。",
"importFailFormat": "匯入失敗,請檢查檔案格式。",
"saveFail": "儲存失敗",
"confirmClearDailyOut": "確定要清除{{itemCode}}的設定排期每天出貨量嗎?",
"clearFail": "清除失敗",
"updateFail": "設定失敗",
"confirmClearScheduleStock": "確定要清除此物料的設定排期庫存嗎?",
"loadRulesFail": "讀取排期規則失敗",
"daysMustBePositive": "可用日必須是大於 0 的數字。",
"triggerNotAboveReplenish": "觸發日不可大於補貨日。",
"categoryNeedsDay": "類別 A / B / C 至少要各選一天可生產。",
"rulesSaved": "已儲存。下次按「預測排期」會用新規則。",
"rulesIntro": "可用日 = 存貨 ÷ 每日平均出貨量。低於觸發日就安排生產,並補到補貨日。留空的物料跟隨這裡的預設。已產生的排期要再按「預測排期」才會更新。",
"defaultRules": "預設規則",
"normalDays": "一般日",
"extraDays": "多備日",
"below": "低於",
"produceUpTo": "日生產",
"topUpTo": "補至",
"days": "日",
"extraWeekdays": "多備星期",
"noProductionUncategorised": "未分類物料不生產",
"skipExtra": "不套用多備",
"saveDefaults": "儲存預設",
"perItem": "個別物料",
"searchItem": "搜尋物料編號或名稱",
"customOnly": "只顯示已自訂",
"triggerDays": "觸發日",
"replenishDays": "補貨日",
"productionDays": "可生產日",
"extraStock": "多備",
"daysBlankFollowDefault": "可用日必須是大於 0 的數字,或留空跟隨預設。",
"customDaysRequired": "自訂可生產日至少要選一天。",
"defaultPlaceholder": "預設",
"custom": "自訂",
"followDefault": "跟隨預設",
"applyExtra": "套用多備",
"clearItemRule": "清除此物料的自訂規則",
"weekdayMon": "一",
"weekdayTue": "二",
"weekdayWed": "三",
"weekdayThu": "四",
"weekdayFri": "五",
"weekdaySat": "六",
"weekdaySun": "日"
}

Loading…
取消
儲存