|
- "use client";
-
- import React, { useEffect, useRef, useState } from "react";
- import {
- Box,
- Stack,
- Typography,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- CircularProgress,
- Paper,
- Table,
- TableHead,
- TableRow,
- TableCell,
- TableBody,
- Button,
- TextField,
- Checkbox,
- FormControlLabel,
- IconButton,
- } from "@mui/material";
- import type { BomCombo, BomDetailResponse } from "@/app/api/bom";
- import {
- editBomClient,
- fetchBomComboClient,
- fetchBomDetailClient,
- fetchAllEquipmentsMasterClient,
- fetchAllProcessesMasterClient,
- type EquipmentMasterRow,
- type ProcessMasterRow,
- } from "@/app/api/bom/client";
- import { useTranslation } from "react-i18next";
- import SearchBox, { Criterion } from "../SearchBox";
- import { useMemo, useCallback } from "react";
- import AddIcon from "@mui/icons-material/Add";
- 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[]>([]);
- const [selectedBomId, setSelectedBomId] = useState<number | "">("");
- const [detail, setDetail] = useState<BomDetailResponse | null>(null);
- const [loadingList, setLoadingList] = useState(false);
- const [loadingDetail, setLoadingDetail] = useState(false);
- const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([])
- const [currentBom, setCurrentBom] = useState<BomCombo | null>(null);
- const loadDetailInFlightRef = useRef(false);
-
- type EditMaterialRow = {
- key: string;
- id?: number;
- itemCode?: string;
- itemName?: string;
- qty: number;
- isConsumable: boolean;
-
- baseUom?: string;
- stockQty?: number;
- stockUom?: string;
- salesQty?: number;
- salesUom?: string;
- };
-
- type EditProcessRow = {
- key: string;
- id?: number;
- seqNo?: number;
- processCode?: string;
- processName?: string;
- description: string;
- /** 設備主檔 description(下拉),與 equipmentName 一併解析為 equipment.code */
- equipmentDescription: string;
- equipmentName: string;
- durationInMinute: number;
- prepTimeInMinute: number;
- postProdTimeInMinute: number;
- };
-
- const [isEditing, setIsEditing] = useState(false);
- const [editLoading, setEditLoading] = useState(false);
- const [editError, setEditError] = useState<string | null>(null);
- const [editBasic, setEditBasic] = useState<{
- description: string;
- outputQty: number;
- outputQtyUom: string;
-
- isDark: number;
- isFloat: number;
- isDense: number;
- scrapRate: number;
- allergicSubstances: number;
- timeSequence: number;
- complexity: number;
- isDrink: boolean;
- } | null>(null);
-
- const [editMaterials, setEditMaterials] = useState<EditMaterialRow[]>([]);
- const [editProcesses, setEditProcesses] = useState<EditProcessRow[]>([]);
-
- 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;
- equipmentDescription: string;
- equipmentName: string;
- description: string;
- durationInMinute: number;
- prepTimeInMinute: number;
- postProdTimeInMinute: number;
- }>({
- processCode: "",
- equipmentDescription: "",
- equipmentName: "",
- description: "",
- durationInMinute: 0,
- prepTimeInMinute: 0,
- postProdTimeInMinute: 0,
- });
-
- const processCodeOptions = useMemo(() => {
- const codes = new Set<string>();
- processMasterList.forEach((p) => {
- if (p.code) codes.add(p.code);
- });
- return Array.from(codes).sort();
- }, [processMasterList]);
-
- const equipmentDescriptionOptions = useMemo(() => {
- const s = new Set<string>();
- equipmentMasterList.forEach((e) => {
- if (e.description) s.add(e.description);
- });
- 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 () => {
- setLoadingList(true);
- try {
- const list = await fetchBomComboClient();
- setBomList(list);
- } finally {
- setLoadingList(false);
- }
- };
- loadList();
- }, []);
- type BomSearchKey = "code" | "name";
-
- const searchCriteria: Criterion<BomSearchKey>[] = useMemo(
- () => [
- { label: t("Code"), paramName: "code", type: "text" },
- { label: t("Name"), paramName: "name", type: "text" },
- ],
- [t],
- );
- useEffect(() => {
- setFilteredBoms([]);
- }, [bomList]);
- const loadBomDetail = useCallback(
- async (id: number) => {
- if (!id || loadDetailInFlightRef.current) return;
- loadDetailInFlightRef.current = true;
- setSelectedBomId(id);
- setCurrentBom(bomList.find((b) => b.id === id) ?? null);
- setDetail(null);
- setLoadingDetail(true);
- try {
- const d = await fetchBomDetailClient(id);
- setDetail(d);
- } finally {
- setLoadingDetail(false);
- loadDetailInFlightRef.current = false;
- }
- },
- [bomList],
- );
-
- const handleSearchBom = useCallback(
- (inputs: Record<BomSearchKey | `${BomSearchKey}To`, string>) => {
- const code = (inputs.code ?? "").trim().toLowerCase();
- const name = (inputs.name ?? "").trim().toLowerCase();
-
- const result = bomList.filter((b) => {
- const label = String(b.label ?? "").toLowerCase();
- const okCode = !code || label.includes(code);
- const okName = !name || label.includes(name);
- return okCode && okName;
- });
-
- setFilteredBoms(result);
-
- // 如果只找到一個,直接載入明細
- if (result.length === 1) {
- void loadBomDetail(result[0].id);
- } else {
- setSelectedBomId("");
- setCurrentBom(null);
- setDetail(null);
- }
- },
- [bomList, loadBomDetail],
- );
- const renderAllergic = (v?: number) => {
- if (v === 0) return "有";
- if (v === 5) return "沒有";
- return "-";
- };
-
- const renderIsFloat = (v?: number) => {
- if (v === 5) return "沉";
- if (v === 3) return "浮";
- if (v === 0) return "不適用";
- return "-";
- };
-
- const renderIsDense = (v?: number) => {
- if (v === 5) return "淡";
- if (v === 3) return "濃";
- if (v === 0) return "不適用";
- return "-";
- };
-
- const renderTimeSequence = (v?: number) => {
- if (v === 5) return "上午";
- if (v === 1) return "下午";
- if (v === 0) return "不適用";
- return "-";
- };
- const renderType = (v?: string) => {
- if (v === "FG") return "成品";
- if (v === "WIP") return "半成品";
- return "-";
- };
-
- const renderComplexity = (v?: number) => {
- if (v === 10) return "簡單";
- if (v === 5) return "中度";
- if (v === 3) return "複雜";
- if (v === 0) return "不適用";
- return "-";
- };
- /*
- const handleResetBom = useCallback(() => {
- setFilteredBoms(bomList);
- setSelectedBomId("");
- setDetail(null);
- }, [bomList]);
- */
- const genKey = () => Math.random().toString(36).slice(2);
-
- const startEdit = useCallback(async () => {
- if (!detail) return;
-
- setEditError(null);
- setEditMasterLoading(true);
- try {
- const [equipments, processes] = await Promise.all([
- fetchAllEquipmentsMasterClient(),
- fetchAllProcessesMasterClient(),
- ]);
- setEquipmentMasterList(equipments);
- setProcessMasterList(processes);
-
- setEditBasic({
- description: detail.description ?? "",
- outputQty: detail.outputQty ?? 0,
- outputQtyUom: detail.outputQtyUom ?? "",
-
- 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,
- });
-
- 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);
- } 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(() => {
- setIsEditing(false);
- setEditLoading(false);
- setEditError(null);
- setEditBasic(null);
- setEditMaterials([]);
- setEditProcesses([]);
- setProcessAddForm({
- processCode: "",
- equipmentDescription: "",
- equipmentName: "",
- description: "",
- durationInMinute: 0,
- prepTimeInMinute: 0,
- postProdTimeInMinute: 0,
- });
- setEquipmentMasterList([]);
- setProcessMasterList([]);
- }, []);
-
- const addMaterialRow = useCallback(() => {
- setEditMaterials((prev) => [
- ...prev,
- {
- key: genKey(),
- itemCode: "",
- itemName: "",
- qty: 0,
- isConsumable: false,
- baseUom: "",
- stockQty: undefined,
- stockUom: "",
- salesQty: undefined,
- salesUom: "",
- },
- ]);
- }, []);
-
- const addProcessRow = useCallback(() => {
- setEditProcesses((prev) => [
- ...prev,
- {
- key: genKey(),
- seqNo: undefined,
- processCode: "",
- processName: "",
- description: "",
- equipmentDescription: "",
- equipmentName: "",
- durationInMinute: 0,
- prepTimeInMinute: 0,
- postProdTimeInMinute: 0,
- },
- ]);
- }, []);
-
- const addProcessFromForm = useCallback(() => {
- const pCode = processAddForm.processCode.trim();
- if (!pCode) {
- setEditError("請先選擇工序 Process Code");
- 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,
- {
- key: genKey(),
- seqNo: undefined,
- processCode: pCode,
- processName: "",
- description: processAddForm.description ?? "",
- equipmentDescription: ed,
- equipmentName: en,
- durationInMinute: processAddForm.durationInMinute ?? 0,
- prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0,
- postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0,
- },
- ]);
-
- setProcessAddForm({
- processCode: "",
- equipmentDescription: "",
- equipmentName: "",
- description: "",
- durationInMinute: 0,
- prepTimeInMinute: 0,
- postProdTimeInMinute: 0,
- });
- setEditError(null);
- }, [processAddForm, equipmentMasterList]);
-
- const deleteMaterialRow = useCallback((key: string) => {
- setEditMaterials((prev) => prev.filter((r) => r.key !== key));
- }, []);
-
- const deleteProcessRow = useCallback((key: string) => {
- setEditProcesses((prev) => prev.filter((r) => r.key !== key));
- }, []);
-
- const handleSaveEdit = useCallback(async () => {
- if (!detail || !editBasic) return;
- setEditLoading(true);
- setEditError(null);
-
- try {
- for (const p of editProcesses) {
- 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 = {
- description: editBasic.description || undefined,
- outputQty: editBasic.outputQty,
- outputQtyUom: editBasic.outputQtyUom || undefined,
-
- isDark: editBasic.isDark,
- isFloat: editBasic.isFloat,
- isDense: editBasic.isDense,
- scrapRate: editBasic.scrapRate,
- allergicSubstances: editBasic.allergicSubstances,
- timeSequence: editBasic.timeSequence,
- complexity: editBasic.complexity,
- isDrink: editBasic.isDrink,
- 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);
- setDetail(updated);
- setIsEditing(false);
- } catch (e: any) {
- setEditError(e?.message || "保存失败");
- } finally {
- setEditLoading(false);
- }
- }, [detail, editBasic, editProcesses, equipmentMasterList]);
-
- return (
- <Stack spacing={2}>
- <SearchBox<BomSearchKey>
- criteria={searchCriteria}
- onSearch={handleSearchBom}
- //onReset={handleResetBom}
- />
-
-
- {filteredBoms.length > 1 && (
- <Paper variant="outlined" sx={{ p: 1.5 }}>
- <Typography variant="subtitle2" sx={{ mb: 1 }}>
- 找到多筆 BOM,請選擇一筆載入明細
- </Typography>
- <Stack direction="row" spacing={1} flexWrap="wrap">
- {filteredBoms.map((b) => (
- <Button
- key={b.id}
- size="small"
- variant={selectedBomId === b.id ? "contained" : "outlined"}
- disabled={loadingDetail}
- onClick={() => void loadBomDetail(b.id)}
- >
- {String(b.label ?? b.id)} ({renderType(b.description)})
- </Button>
- ))}
- </Stack>
- </Paper>
- )}
- {loadingDetail && (
- <Typography variant="body2" color="text.secondary">
- {t("Loading BOM Detail...")}
- </Typography>
- )}
-
- {detail && (
- <Stack spacing={2}>
- <Typography variant="subtitle1">
- {detail.itemCode} {detail.itemName}
- </Typography>
-
- {/* Basic Info 列表 */}
- <Paper variant="outlined" sx={{ p: 2 }}>
- <Stack
- direction="row"
- alignItems="center"
- justifyContent="space-between"
- sx={{ mb: 1 }}
- >
- <Typography variant="subtitle1" gutterBottom>
- {t("Basic Info")}
- </Typography>
-
- {!isEditing ? (
- <Button
- size="small"
- startIcon={
- editMasterLoading ? (
- <CircularProgress size={16} />
- ) : (
- <EditIcon />
- )
- }
- variant="outlined"
- onClick={() => void startEdit()}
- disabled={editMasterLoading}
- >
- {editMasterLoading ? t("Loading...") : t("Edit")}
- </Button>
- ) : (
- <Stack direction="row" spacing={1}>
- <Button
- size="small"
- startIcon={<SaveIcon />}
- variant="contained"
- disabled={editLoading}
- onClick={handleSaveEdit}
- >
- {editLoading ? t("Saving...") : t("Save")}
- </Button>
- <Button
- size="small"
- startIcon={<CancelIcon />}
- variant="outlined"
- disabled={editLoading}
- onClick={cancelEdit}
- >
- {t("Cancel")}
- </Button>
- </Stack>
- )}
- </Stack>
-
- {editError && (
- <Typography variant="body2" color="error">
- {editError}
- </Typography>
- )}
-
- {!isEditing && (
- <Stack spacing={0.5}>
- {/* 第一行:輸出數量 + 類型 */}
- <Typography variant="body2">
- {t("Output Quantity")}: {detail.outputQty} {detail.outputQtyUom}
- {" "}
- {t("Type")}: {detail.description ?? "-"}
- </Typography>
-
- {/* 第二行:各種指標,排成一行 key:value, key:value */}
- <Typography variant="body2">
- {t("Allergic Substances")}:{" "}
- {renderAllergic(detail.allergicSubstances)}
- {" "}{t("Depth")}: {detail.isDark ?? "-"}
- {" "}{t("Float")}: {renderIsFloat(detail.isFloat)}
- {" "}{t("Density")}: {renderIsDense(detail.isDense)}
- </Typography>
-
- <Typography variant="body2">
- {t("Time Sequence")}: {renderTimeSequence(detail.timeSequence)}
- {" "}{t("Complexity")}: {renderComplexity(detail.complexity)}
- {" "}{t("Base Score")}: {detail.baseScore ?? "-"}
- </Typography>
- </Stack>
- )}
-
- {isEditing && editBasic && (
- <Stack spacing={1}>
- <TextField
- size="small"
- label={t("Type")}
- value={editBasic.description}
- onChange={(e) =>
- setEditBasic((p) => (p ? { ...p, description: e.target.value } : p))
- }
- fullWidth
- />
-
- <Stack direction="row" spacing={1}>
- <TextField
- size="small"
- label={t("Output Quantity")}
- type="number"
- value={editBasic.outputQty}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, outputQty: Number(e.target.value) } : p
- )
- }
- />
- <TextField
- size="small"
- label={t("Output Quantity UOM")}
- value={editBasic.outputQtyUom}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, outputQtyUom: e.target.value } : p
- )
- }
- />
- </Stack>
-
- <Stack direction="row" spacing={1}>
- <TextField
- size="small"
- label={t("Scrap Rate")}
- type="number"
- value={editBasic.scrapRate}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, scrapRate: Number(e.target.value) } : p
- )
- }
- />
- <FormControl size="small" sx={{ minWidth: 160 }}>
- <InputLabel>{t("Allergic Substances")}</InputLabel>
- <Select
- label={t("Allergic Substances")}
- value={editBasic.allergicSubstances}
- onChange={(e) =>
- setEditBasic((p) =>
- p
- ? {
- ...p,
- allergicSubstances: Number(e.target.value),
- }
- : p,
- )
- }
- >
- <MenuItem value={0}>有</MenuItem>
- <MenuItem value={5}>沒有</MenuItem>
- </Select>
- </FormControl>
- </Stack>
-
- <Stack direction="row" spacing={1}>
- <TextField
- size="small"
- label={t("Depth")}
- type="number"
- value={editBasic.isDark}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, isDark: Number(e.target.value) } : p
- )
- }
- inputProps={{ min: 1, max: 5, step: 1 }}
- />
- <FormControl size="small" sx={{ minWidth: 140 }}>
- <InputLabel>{t("Float")}</InputLabel>
- <Select
- label={t("Float")}
- value={editBasic.isFloat}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, isFloat: Number(e.target.value) } : p,
- )
- }
- >
- <MenuItem value={5}>沉</MenuItem>
- <MenuItem value={3}>浮</MenuItem>
- <MenuItem value={0}>不適用</MenuItem>
- </Select>
- </FormControl>
- <FormControl size="small" sx={{ minWidth: 140 }}>
- <InputLabel>{t("Density")}</InputLabel>
- <Select
- label={t("Density")}
- value={editBasic.isDense}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, isDense: Number(e.target.value) } : p,
- )
- }
- >
- <MenuItem value={5}>淡</MenuItem>
- <MenuItem value={3}>濃</MenuItem>
- <MenuItem value={0}>不適用</MenuItem>
- </Select>
- </FormControl>
- </Stack>
-
- <Stack direction="row" spacing={1}>
- <FormControl size="small" sx={{ minWidth: 180 }}>
- <InputLabel>{t("Time Sequence")}</InputLabel>
- <Select
- label={t("Time Sequence")}
- value={editBasic.timeSequence}
- onChange={(e) =>
- setEditBasic((p) =>
- p
- ? { ...p, timeSequence: Number(e.target.value) }
- : p,
- )
- }
- >
- <MenuItem value={5}>上午</MenuItem>
- <MenuItem value={1}>下午</MenuItem>
- <MenuItem value={0}>不適用</MenuItem>
- </Select>
- </FormControl>
- <FormControl size="small" sx={{ minWidth: 180 }}>
- <InputLabel>{t("Complexity")}</InputLabel>
- <Select
- label={t("Complexity")}
- value={editBasic.complexity}
- onChange={(e) =>
- setEditBasic((p) =>
- p
- ? { ...p, complexity: Number(e.target.value) }
- : p,
- )
- }
- >
- <MenuItem value={10}>簡單</MenuItem>
- <MenuItem value={5}>中度</MenuItem>
- <MenuItem value={3}>複雜</MenuItem>
- <MenuItem value={0}>不適用</MenuItem>
- </Select>
- </FormControl>
- </Stack>
-
- <FormControlLabel
- control={
- <Checkbox
- checked={editBasic.isDrink}
- onChange={(e) =>
- setEditBasic((p) =>
- p ? { ...p, isDrink: e.target.checked } : p
- )
- }
- />
- }
- label={t("Is Drink")}
- />
- </Stack>
- )}
- </Paper>
- {/* 材料列表 */}
- <Paper variant="outlined" sx={{ p: 2 }}>
- <Typography variant="subtitle1" gutterBottom>
- {t("Bom Material")}
- </Typography>
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell> {t("Item Code")}</TableCell>
- <TableCell> {t("Item Name")}</TableCell>
- <TableCell align="right"> {t("Base Qty")}</TableCell>
- <TableCell> {t("Base UOM")}</TableCell>
- <TableCell align="right"> {t("Stock Qty")}</TableCell>
- <TableCell> {t("Stock UOM")}</TableCell>
- <TableCell align="right"> {t("Sales Qty")}</TableCell>
- <TableCell> {t("Sales UOM")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {detail.materials.map((m, i) => (
- <TableRow key={i}>
- <TableCell>{m.itemCode}</TableCell>
- <TableCell>{m.itemName}</TableCell>
- <TableCell align="right">{m.baseQty}</TableCell>
- <TableCell>{m.baseUom}</TableCell>
- <TableCell align="right">{m.stockQty}</TableCell>
- <TableCell>{m.stockUom}</TableCell>
- <TableCell align="right">{m.salesQty}</TableCell>
- <TableCell>{m.salesUom}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </Paper>
-
- {/* 製程 + 設備列表 */}
- <Paper variant="outlined" sx={{ p: 2 }}>
- <Typography variant="subtitle1" gutterBottom>
- {t("Process & Equipment")}
- </Typography>
- {isEditing && (
- <Box sx={{ mb: 1 }}>
- <Stack direction="row" spacing={1} flexWrap="wrap">
- <FormControl size="small" sx={{ minWidth: 200 }}>
- <InputLabel>{t("Process Code")}</InputLabel>
- <Select
- label={t("Process Code")}
- value={processAddForm.processCode}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- processCode: String(e.target.value),
- }))
- }
- >
- <MenuItem value="">
- <em>請選擇</em>
- </MenuItem>
- {processCodeOptions.map((c) => (
- <MenuItem key={c} value={c}>
- {c}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
-
- <FormControl size="small" sx={{ minWidth: 200 }}>
- <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="設備名稱"
- value={processAddForm.equipmentName}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- equipmentName: String(e.target.value),
- }))
- }
- >
- <MenuItem value="">不適用</MenuItem>
- {equipmentNameOptions.map((c) => (
- <MenuItem key={c} value={c}>
- {c}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
-
- <TextField
- size="small"
- label={t("Process Description")}
- value={processAddForm.description}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- description: e.target.value,
- }))
- }
- />
-
- <TextField
- size="small"
- label={t("Duration (Minutes)")}
- type="number"
- value={processAddForm.durationInMinute}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- durationInMinute: Number(e.target.value),
- }))
- }
- />
- <TextField
- size="small"
- label={t("Prep Time (Minutes)")}
- type="number"
- value={processAddForm.prepTimeInMinute}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- prepTimeInMinute: Number(e.target.value),
- }))
- }
- />
- <TextField
- size="small"
- label={t("Post Prod Time (Minutes)")}
- type="number"
- value={processAddForm.postProdTimeInMinute}
- onChange={(e) =>
- setProcessAddForm((p) => ({
- ...p,
- postProdTimeInMinute: Number(e.target.value),
- }))
- }
- />
-
- <Button
- size="small"
- startIcon={<AddIcon />}
- variant="contained"
- onClick={addProcessFromForm}
- >
- {t("Add")}
- </Button>
- </Stack>
- </Box>
- )}
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell> {t("Sequence")}</TableCell>
- <TableCell> {t("Process Name")}</TableCell>
- <TableCell> {t("Process Description")}</TableCell>
- <TableCell> {t("Process 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>
- {isEditing && (
- <TableCell align="right">{t("Actions")}</TableCell>
- )}
- </TableRow>
- </TableHead>
- <TableBody>
- {isEditing
- ? editProcesses.map((p) => (
- <TableRow key={p.key}>
- <TableCell>{p.seqNo ?? "-"}</TableCell>
- <TableCell>{p.processName || "-"}</TableCell>
- <TableCell>
- <TextField
- size="small"
- value={p.description}
- onChange={(e) =>
- setEditProcesses((prev) =>
- prev.map((x) =>
- x.key === p.key
- ? { ...x, description: e.target.value }
- : x,
- ),
- )
- }
- />
- </TableCell>
- <TableCell>
- <FormControl size="small" fullWidth>
- <Select
- value={p.processCode ?? ""}
- onChange={(e) =>
- setEditProcesses((prev) =>
- prev.map((x) =>
- x.key === p.key
- ? {
- ...x,
- processCode: String(e.target.value),
- }
- : x,
- ),
- )
- }
- >
- <MenuItem value="">不適用</MenuItem>
- {processCodeOptions.map((c) => (
- <MenuItem key={c} value={c}>
- {c}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- </TableCell>
- <TableCell>
- <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
- size="small"
- type="number"
- value={p.durationInMinute}
- onChange={(e) =>
- setEditProcesses((prev) =>
- prev.map((x) =>
- x.key === p.key
- ? { ...x, durationInMinute: Number(e.target.value) }
- : x,
- ),
- )
- }
- />
- </TableCell>
- <TableCell align="right">
- <TextField
- size="small"
- type="number"
- value={p.prepTimeInMinute}
- onChange={(e) =>
- setEditProcesses((prev) =>
- prev.map((x) =>
- x.key === p.key
- ? { ...x, prepTimeInMinute: Number(e.target.value) }
- : x,
- ),
- )
- }
- />
- </TableCell>
- <TableCell align="right">
- <TextField
- size="small"
- type="number"
- value={p.postProdTimeInMinute}
- onChange={(e) =>
- setEditProcesses((prev) =>
- prev.map((x) =>
- x.key === p.key
- ? {
- ...x,
- postProdTimeInMinute: Number(e.target.value),
- }
- : x,
- ),
- )
- }
- />
- </TableCell>
- <TableCell align="right">
- <IconButton size="small" onClick={() => deleteProcessRow(p.key)}>
- <DeleteIcon fontSize="small" />
- </IconButton>
- </TableCell>
- </TableRow>
- ))
- : detail.processes.map((p, i) => (
- <TableRow key={i}>
- <TableCell>{p.seqNo}</TableCell>
- <TableCell>{p.processName}</TableCell>
- <TableCell>{p.processDescription}</TableCell>
- <TableCell>{p.processCode ?? "-"}</TableCell>
- <TableCell>{p.equipmentCode ?? p.equipmentName}</TableCell>
- <TableCell align="right">{p.durationInMinute}</TableCell>
- <TableCell align="right">{p.prepTimeInMinute}</TableCell>
- <TableCell align="right">
- {p.postProdTimeInMinute}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </Paper>
- </Stack>
- )}
- </Stack>
- );
- };
-
- export default ImportBomDetailTab;
|