ソースを参照

update

MergeProblem1
CANCERYS\kw093 12時間前
コミット
7dc1fbf323
4個のファイルの変更329行の追加130行の削除
  1. +41
    -1
      src/app/api/bom/client.ts
  2. +280
    -107
      src/components/ImportBom/ImportBomDetailTab.tsx
  3. +1
    -2
      src/components/PickOrderSearch/AssignAndRelease.tsx
  4. +7
    -20
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx

+ 41
- 1
src/app/api/bom/client.ts ファイルの表示

@@ -115,4 +115,44 @@ export async function fetchBomComboClient(): Promise<BomCombo[]> {
{ params: { batchId } }
);
return response.data;
}
}

/** Master `equipment` rows for BOM process editor (description/name → code). */
export type EquipmentMasterRow = {
code: string;
name: string;
description: string;
};

/** Master `process` rows for BOM process editor (dropdown by code). */
export type ProcessMasterRow = {
code: string;
name: string;
};

export async function fetchAllEquipmentsMasterClient(): Promise<
EquipmentMasterRow[]
> {
const response = await axiosInstance.get<unknown[]>(
`${NEXT_PUBLIC_API_URL}/Equipment`,
);
const rows = Array.isArray(response.data) ? response.data : [];
return rows.map((r: any) => ({
code: String(r?.code ?? "").trim(),
name: String(r?.name ?? "").trim(),
description: String(r?.description ?? "").trim(),
}));
}

export async function fetchAllProcessesMasterClient(): Promise<
ProcessMasterRow[]
> {
const response = await axiosInstance.get<unknown[]>(
`${NEXT_PUBLIC_API_URL}/Process`,
);
const rows = Array.isArray(response.data) ? response.data : [];
return rows.map((r: any) => ({
code: String(r?.code ?? "").trim(),
name: String(r?.name ?? "").trim(),
}));
}

+ 280
- 107
src/components/ImportBom/ImportBomDetailTab.tsx ファイルの表示

@@ -27,6 +27,10 @@ import {
editBomClient,
fetchBomComboClient,
fetchBomDetailClient,
fetchAllEquipmentsMasterClient,
fetchAllProcessesMasterClient,
type EquipmentMasterRow,
type ProcessMasterRow,
} from "@/app/api/bom/client";
import type { SelectChangeEvent } from "@mui/material/Select";
import { useTranslation } from "react-i18next";
@@ -37,6 +41,26 @@ import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";

/** 以 description + "-" + name 對應 code,或同一筆設備的 description+name。 */
function resolveEquipmentCode(
list: EquipmentMasterRow[],
description: string,
name: string,
): string | null {
const d = description.trim();
const n = name.trim();
if (!d && !n) return null;
if (!d || !n) return null;
const composite = `${d}-${n}`;
const byCode = list.find((e) => e.code === composite);
if (byCode) return byCode.code;
const byPair = list.find(
(e) => e.description === d && e.name === n,
);
return byPair?.code ?? null;
}

const ImportBomDetailTab: React.FC = () => {
const { t } = useTranslation( "common" );
const [bomList, setBomList] = useState<BomCombo[]>([]);
@@ -69,7 +93,9 @@ const ImportBomDetailTab: React.FC = () => {
processCode?: string;
processName?: string;
description: string;
equipmentCode?: string;
/** 設備主檔 description(下拉),與 equipmentName 一併解析為 equipment.code */
equipmentDescription: string;
equipmentName: string;
durationInMinute: number;
prepTimeInMinute: number;
postProdTimeInMinute: number;
@@ -96,17 +122,27 @@ const ImportBomDetailTab: React.FC = () => {
const [editMaterials, setEditMaterials] = useState<EditMaterialRow[]>([]);
const [editProcesses, setEditProcesses] = useState<EditProcessRow[]>([]);

// Process add form (uses dropdown selections).
const [equipmentMasterList, setEquipmentMasterList] = useState<
EquipmentMasterRow[]
>([]);
const [processMasterList, setProcessMasterList] = useState<
ProcessMasterRow[]
>([]);
const [editMasterLoading, setEditMasterLoading] = useState(false);

// Process add form (uses dropdown selections from master tables).
const [processAddForm, setProcessAddForm] = useState<{
processCode: string;
equipmentCode: string;
equipmentDescription: string;
equipmentName: string;
description: string;
durationInMinute: number;
prepTimeInMinute: number;
postProdTimeInMinute: number;
}>({
processCode: "",
equipmentCode: "",
equipmentDescription: "",
equipmentName: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
@@ -115,19 +151,27 @@ const ImportBomDetailTab: React.FC = () => {

const processCodeOptions = useMemo(() => {
const codes = new Set<string>();
(detail?.processes ?? []).forEach((p) => {
if (p.processCode) codes.add(p.processCode);
processMasterList.forEach((p) => {
if (p.code) codes.add(p.code);
});
return Array.from(codes);
}, [detail]);
return Array.from(codes).sort();
}, [processMasterList]);

const equipmentCodeOptions = useMemo(() => {
const codes = new Set<string>();
(detail?.processes ?? []).forEach((p) => {
if (p.equipmentCode) codes.add(p.equipmentCode);
const equipmentDescriptionOptions = useMemo(() => {
const s = new Set<string>();
equipmentMasterList.forEach((e) => {
if (e.description) s.add(e.description);
});
return Array.from(codes);
}, [detail]);
return Array.from(s).sort();
}, [equipmentMasterList]);

const equipmentNameOptions = useMemo(() => {
const s = new Set<string>();
equipmentMasterList.forEach((e) => {
if (e.name) s.add(e.name);
});
return Array.from(s).sort();
}, [equipmentMasterList]);

useEffect(() => {
const loadList = async () => {
@@ -242,57 +286,82 @@ const ImportBomDetailTab: React.FC = () => {

const genKey = () => Math.random().toString(36).slice(2);

const startEdit = useCallback(() => {
const startEdit = useCallback(async () => {
if (!detail) return;

setEditError(null);
setEditBasic({
description: detail.description ?? "",
outputQty: detail.outputQty ?? 0,
outputQtyUom: detail.outputQtyUom ?? "",
setEditMasterLoading(true);
try {
const [equipments, processes] = await Promise.all([
fetchAllEquipmentsMasterClient(),
fetchAllProcessesMasterClient(),
]);
setEquipmentMasterList(equipments);
setProcessMasterList(processes);

isDark: detail.isDark ?? 0,
isFloat: detail.isFloat ?? 0,
isDense: detail.isDense ?? 0,
scrapRate: detail.scrapRate ?? 0,
allergicSubstances: detail.allergicSubstances ?? 0,
timeSequence: detail.timeSequence ?? 0,
complexity: detail.complexity ?? 0,
isDrink: detail.isDrink ?? false,
});
setEditBasic({
description: detail.description ?? "",
outputQty: detail.outputQty ?? 0,
outputQtyUom: detail.outputQtyUom ?? "",

setEditMaterials(
(detail.materials ?? []).map((m) => ({
key: genKey(),
id: undefined,
itemCode: m.itemCode ?? "",
itemName: m.itemName ?? "",
qty: m.baseQty ?? 0,
isConsumable: m.isConsumable ?? false,
baseUom: m.baseUom,
stockQty: m.stockQty,
stockUom: m.stockUom,
salesQty: m.salesQty,
salesUom: m.salesUom,
})),
);
isDark: detail.isDark ?? 0,
isFloat: detail.isFloat ?? 0,
isDense: detail.isDense ?? 0,
scrapRate: detail.scrapRate ?? 0,
allergicSubstances: detail.allergicSubstances ?? 0,
timeSequence: detail.timeSequence ?? 0,
complexity: detail.complexity ?? 0,
isDrink: detail.isDrink ?? false,
});

setEditProcesses(
(detail.processes ?? []).map((p) => ({
key: genKey(),
id: undefined,
seqNo: p.seqNo,
processCode: p.processCode ?? "",
processName: p.processName,
description: p.processDescription ?? "",
equipmentCode: p.equipmentCode ?? p.equipmentName ?? "",
durationInMinute: p.durationInMinute ?? 0,
prepTimeInMinute: p.prepTimeInMinute ?? 0,
postProdTimeInMinute: p.postProdTimeInMinute ?? 0,
})),
);
setEditMaterials(
(detail.materials ?? []).map((m) => ({
key: genKey(),
id: undefined,
itemCode: m.itemCode ?? "",
itemName: m.itemName ?? "",
qty: m.baseQty ?? 0,
isConsumable: m.isConsumable ?? false,
baseUom: m.baseUom,
stockQty: m.stockQty,
stockUom: m.stockUom,
salesQty: m.salesQty,
salesUom: m.salesUom,
})),
);

setEditProcesses(
(detail.processes ?? []).map((p) => {
const code = (p.equipmentCode ?? "").trim();
const eq = code
? equipments.find((e) => e.code === code)
: undefined;
return {
key: genKey(),
id: undefined,
seqNo: p.seqNo,
processCode: p.processCode ?? "",
processName: p.processName,
description: p.processDescription ?? "",
equipmentDescription: eq?.description ?? "",
equipmentName: eq?.name ?? "",
durationInMinute: p.durationInMinute ?? 0,
prepTimeInMinute: p.prepTimeInMinute ?? 0,
postProdTimeInMinute: p.postProdTimeInMinute ?? 0,
};
}),
);

setIsEditing(true);
setIsEditing(true);
} catch (e: unknown) {
const msg =
e && typeof e === "object" && "message" in e
? String((e as { message?: string }).message)
: "載入製程/設備主檔失敗";
setEditError(msg);
} finally {
setEditMasterLoading(false);
}
}, [detail]);

const cancelEdit = useCallback(() => {
@@ -304,12 +373,15 @@ const ImportBomDetailTab: React.FC = () => {
setEditProcesses([]);
setProcessAddForm({
processCode: "",
equipmentCode: "",
equipmentDescription: "",
equipmentName: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
});
setEquipmentMasterList([]);
setProcessMasterList([]);
}, []);

const addMaterialRow = useCallback(() => {
@@ -339,7 +411,8 @@ const ImportBomDetailTab: React.FC = () => {
processCode: "",
processName: "",
description: "",
equipmentCode: "",
equipmentDescription: "",
equipmentName: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
@@ -354,6 +427,22 @@ const ImportBomDetailTab: React.FC = () => {
return;
}

const ed = processAddForm.equipmentDescription.trim();
const en = processAddForm.equipmentName.trim();
if ((ed && !en) || (!ed && en)) {
setEditError("設備描述與名稱需同時選取,或同時留空(不適用)");
return;
}
if (ed && en) {
const resolved = resolveEquipmentCode(equipmentMasterList, ed, en);
if (!resolved) {
setEditError(
`設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`,
);
return;
}
}

setEditProcesses((prev) => [
...prev,
{
@@ -362,7 +451,8 @@ const ImportBomDetailTab: React.FC = () => {
processCode: pCode,
processName: "",
description: processAddForm.description ?? "",
equipmentCode: processAddForm.equipmentCode.trim(),
equipmentDescription: ed,
equipmentName: en,
durationInMinute: processAddForm.durationInMinute ?? 0,
prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0,
postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0,
@@ -371,14 +461,15 @@ const ImportBomDetailTab: React.FC = () => {

setProcessAddForm({
processCode: "",
equipmentCode: "",
equipmentDescription: "",
equipmentName: "",
description: "",
durationInMinute: 0,
prepTimeInMinute: 0,
postProdTimeInMinute: 0,
});
setEditError(null);
}, [processAddForm]);
}, [processAddForm, equipmentMasterList]);

const deleteMaterialRow = useCallback((key: string) => {
setEditMaterials((prev) => prev.filter((r) => r.key !== key));
@@ -398,6 +489,19 @@ const ImportBomDetailTab: React.FC = () => {
if (!p.processCode?.trim()) {
throw new Error("工序行 Process Code 不能为空");
}
const ed = p.equipmentDescription.trim();
const en = p.equipmentName.trim();
if ((ed && !en) || (!ed && en)) {
throw new Error("各製程行的設備描述與名稱需同時填寫或同時留空");
}
if (ed && en) {
const resolved = resolveEquipmentCode(equipmentMasterList, ed, en);
if (!resolved) {
throw new Error(
`設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`,
);
}
}
}

const payload: any = {
@@ -413,16 +517,24 @@ const ImportBomDetailTab: React.FC = () => {
timeSequence: editBasic.timeSequence,
complexity: editBasic.complexity,
isDrink: editBasic.isDrink,
processes: editProcesses.map((p) => ({
id: p.id,
seqNo: p.seqNo,
processCode: p.processCode?.trim() || undefined,
equipmentCode: p.equipmentCode?.trim() || undefined,
description: p.description || undefined,
durationInMinute: p.durationInMinute,
prepTimeInMinute: p.prepTimeInMinute,
postProdTimeInMinute: p.postProdTimeInMinute,
})),
processes: editProcesses.map((p) => {
const ed = p.equipmentDescription.trim();
const en = p.equipmentName.trim();
const equipmentCode =
ed && en
? resolveEquipmentCode(equipmentMasterList, ed, en) ?? undefined
: undefined;
return {
id: p.id,
seqNo: p.seqNo,
processCode: p.processCode?.trim() || undefined,
equipmentCode,
description: p.description || undefined,
durationInMinute: p.durationInMinute,
prepTimeInMinute: p.prepTimeInMinute,
postProdTimeInMinute: p.postProdTimeInMinute,
};
}),
};

const updated = await editBomClient(detail.id, payload);
@@ -433,7 +545,7 @@ const ImportBomDetailTab: React.FC = () => {
} finally {
setEditLoading(false);
}
}, [detail, editBasic, editProcesses]);
}, [detail, editBasic, editProcesses, equipmentMasterList]);

return (
<Stack spacing={2}>
@@ -480,11 +592,18 @@ const ImportBomDetailTab: React.FC = () => {
{!isEditing ? (
<Button
size="small"
startIcon={<EditIcon />}
startIcon={
editMasterLoading ? (
<CircularProgress size={16} />
) : (
<EditIcon />
)
}
variant="outlined"
onClick={startEdit}
onClick={() => void startEdit()}
disabled={editMasterLoading}
>
{t("Edit")}
{editMasterLoading ? t("Loading...") : t("Edit")}
</Button>
) : (
<Stack direction="row" spacing={1}>
@@ -770,6 +889,9 @@ const ImportBomDetailTab: React.FC = () => {
}))
}
>
<MenuItem value="">
<em>請選擇</em>
</MenuItem>
{processCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
@@ -779,19 +901,40 @@ const ImportBomDetailTab: React.FC = () => {
</FormControl>

<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>{t("Equipment Code")}</InputLabel>
<InputLabel>設備說明</InputLabel>
<Select
label="設備說明"
value={processAddForm.equipmentDescription}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
equipmentDescription: String(e.target.value),
}))
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentDescriptionOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>

<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>設備名稱</InputLabel>
<Select
label={t("Equipment Code")}
value={processAddForm.equipmentCode}
label="設備名稱"
value={processAddForm.equipmentName}
onChange={(e) =>
setProcessAddForm((p) => ({
...p,
equipmentCode: String(e.target.value),
equipmentName: String(e.target.value),
}))
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentCodeOptions.map((c) => (
{equipmentNameOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
@@ -866,7 +1009,7 @@ const ImportBomDetailTab: React.FC = () => {
<TableCell> {t("Process Name")}</TableCell>
<TableCell> {t("Process Description")}</TableCell>
<TableCell> {t("Process Code")}</TableCell>
<TableCell> {t("Equipment Code")}</TableCell>
<TableCell>設備(說明/名稱)</TableCell>
<TableCell align="right"> {t("Duration (Minutes)")}</TableCell>
<TableCell align="right"> {t("Prep Time (Minutes)")}</TableCell>
<TableCell align="right"> {t("Post Prod Time (Minutes)")}</TableCell>
@@ -923,30 +1066,60 @@ const ImportBomDetailTab: React.FC = () => {
</FormControl>
</TableCell>
<TableCell>
<FormControl size="small" fullWidth>
<Select
value={p.equipmentCode ?? ""}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
equipmentCode: String(e.target.value),
}
: x,
),
)
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentCodeOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>
<Stack direction="row" spacing={0.5} flexWrap="wrap">
<FormControl size="small" sx={{ minWidth: 140 }}>
<Select
displayEmpty
value={p.equipmentDescription}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
equipmentDescription: String(
e.target.value,
),
}
: x,
),
)
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentDescriptionOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 140 }}>
<Select
displayEmpty
value={p.equipmentName}
onChange={(e) =>
setEditProcesses((prev) =>
prev.map((x) =>
x.key === p.key
? {
...x,
equipmentName: String(e.target.value),
}
: x,
),
)
}
>
<MenuItem value="">不適用</MenuItem>
{equipmentNameOptions.map((c) => (
<MenuItem key={c} value={c}>
{c}
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
</TableCell>
<TableCell align="right">
<TextField


+ 1
- 2
src/components/PickOrderSearch/AssignAndRelease.tsx ファイルの表示

@@ -497,8 +497,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
{/* Target Date - 只在第一个项目显示 */}
<TableCell>
{index === 0 ? (
arrayToDayjs(item.targetDate)
.add(-1, "month")
arrayToDayjs(item.targetDate)
.format(OUTPUT_DATE_FORMAT)
) : null}
</TableCell>


+ 7
- 20
src/components/StockTakeManagement/ApproverStockTakeAll.tsx ファイルの表示

@@ -181,6 +181,10 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
(detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0
? "second"
: "first");
// 避免 Approver 手动输入过程中被 variance 过滤掉,导致“输入后行消失无法提交”
if (selection === "approver") {
return true;
}
const difference = calculateDifference(detail, selection);
const bookQty =
detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
@@ -230,26 +234,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
finalQty = detail.secondStockTakeQty;
finalBadQty = detail.secondBadQty || 0;
} else {
const approverQtyValue = approverQty[detail.id];
const approverBadQtyValue = approverBadQty[detail.id];

if (
approverQtyValue === undefined ||
approverQtyValue === null ||
approverQtyValue === ""
) {
onSnackbar(t("Please enter Approver QTY"), "error");
return;
}
if (
approverBadQtyValue === undefined ||
approverBadQtyValue === null ||
approverBadQtyValue === ""
) {
onSnackbar(t("Please enter Approver Bad QTY"), "error");
return;
}

// 与 Picker 逻辑一致:Approver 输入为空时按 0 处理
const approverQtyValue = approverQty[detail.id] || "0";
const approverBadQtyValue = approverBadQty[detail.id] || "0";
finalQty = parseFloat(approverQtyValue) || 0;
finalBadQty = parseFloat(approverBadQtyValue) || 0;
}


読み込み中…
キャンセル
保存