| @@ -182,6 +182,20 @@ export async function fetchBomComboClient(options?: { | |||
| return response.data; | |||
| } | |||
| export async function downloadBomDetailExcelClient( | |||
| id: number, | |||
| fileName: string, | |||
| ): Promise<{ blob: Blob; fileName: string }> { | |||
| const response = await axiosInstance.get( | |||
| `${NEXT_PUBLIC_API_URL}/bom/${id}/export-excel`, | |||
| { responseType: "blob" }, | |||
| ); | |||
| return { | |||
| blob: response.data as Blob, | |||
| fileName, | |||
| }; | |||
| } | |||
| export async function editBomClient( | |||
| id: number, | |||
| request: EditBomRequest, | |||
| @@ -14,6 +14,7 @@ import { | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import FileDownloadIcon from "@mui/icons-material/FileDownload"; | |||
| import type { BomVersionSummary } from "@/app/api/bom"; | |||
| import { fetchBomVersionsClient } from "@/app/api/bom/client"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -26,12 +27,15 @@ type Props = { | |||
| compareMode: boolean; | |||
| compareToId: number | ""; | |||
| saving: boolean; | |||
| exporting?: boolean; | |||
| versionsRefreshKey?: number; | |||
| saveDisabled: boolean; | |||
| exportDisabled?: boolean; | |||
| saveError?: string | null; | |||
| onVersionChange: (id: number) => void; | |||
| onToggleCompare: () => void; | |||
| onCompareToChange: (id: number) => void; | |||
| onExport?: () => void; | |||
| onSave: () => void; | |||
| }; | |||
| @@ -63,12 +67,15 @@ export const BomVersionControlBar: React.FC<Props> = ({ | |||
| compareMode, | |||
| compareToId, | |||
| saving, | |||
| exporting = false, | |||
| versionsRefreshKey = 0, | |||
| saveDisabled, | |||
| exportDisabled = false, | |||
| saveError, | |||
| onVersionChange, | |||
| onToggleCompare, | |||
| onCompareToChange, | |||
| onExport, | |||
| onSave, | |||
| }) => { | |||
| const { t } = useTranslation("importBom"); | |||
| @@ -94,7 +101,7 @@ export const BomVersionControlBar: React.FC<Props> = ({ | |||
| }, [bomCode, bomKind, versionsRefreshKey]); | |||
| const showCompareButton = versions.length >= 2; | |||
| const controlsDisabled = loadingVersions || saving; | |||
| const controlsDisabled = loadingVersions || saving || exporting; | |||
| const comparingTwo = | |||
| compareMode && | |||
| versionId !== "" && | |||
| @@ -189,6 +196,23 @@ export const BomVersionControlBar: React.FC<Props> = ({ | |||
| </Stack> | |||
| <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap"> | |||
| {onExport && ( | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| startIcon={ | |||
| exporting ? ( | |||
| <CircularProgress size={16} color="inherit" /> | |||
| ) : ( | |||
| <FileDownloadIcon /> | |||
| ) | |||
| } | |||
| disabled={saving || exporting || exportDisabled} | |||
| onClick={onExport} | |||
| > | |||
| {exporting ? t("Downloading...") : t("Export Excel")} | |||
| </Button> | |||
| )} | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| @@ -199,7 +223,7 @@ export const BomVersionControlBar: React.FC<Props> = ({ | |||
| <SaveIcon /> | |||
| ) | |||
| } | |||
| disabled={saving || saveDisabled} | |||
| disabled={saving || exporting || saveDisabled} | |||
| onClick={onSave} | |||
| > | |||
| {saving ? t("Saving...") : t("Save")} | |||
| @@ -32,6 +32,7 @@ import { | |||
| fetchBomDetailClient, | |||
| fetchAllEquipmentsMasterClient, | |||
| fetchAllProcessesMasterClient, | |||
| downloadBomDetailExcelClient, | |||
| type EquipmentMasterRow, | |||
| type ProcessMasterRow, | |||
| } from "@/app/api/bom/client"; | |||
| @@ -100,6 +101,7 @@ const ImportBomDetailTab: React.FC = () => { | |||
| const [currentBom, setCurrentBom] = useState<BomCombo | null>(null); | |||
| const loadDetailInFlightRef = useRef(false); | |||
| const saveInFlightRef = useRef(false); | |||
| const exportInFlightRef = useRef(false); | |||
| const skipAutoSearchRef = useRef(false); | |||
| const detailIdRef = useRef<number | null>(null); | |||
| const lastSearchInputsRef = useRef<BomSearchInputs>(EMPTY_BOM_SEARCH_INPUTS); | |||
| @@ -164,6 +166,7 @@ const ImportBomDetailTab: React.FC = () => { | |||
| >([]); | |||
| const [editMasterLoading, setEditMasterLoading] = useState(false); | |||
| const [saving, setSaving] = useState(false); | |||
| const [exporting, setExporting] = useState(false); | |||
| const [saveError, setSaveError] = useState<string | null>(null); | |||
| const [versionId, setVersionId] = useState<number | "">(""); | |||
| const [compareMode, setCompareMode] = useState(false); | |||
| @@ -549,6 +552,32 @@ const ImportBomDetailTab: React.FC = () => { | |||
| unresolvedMaterialIssueCount, | |||
| ]); | |||
| const handleExportExcel = useCallback(async () => { | |||
| if (typeof versionId !== "number" || exportInFlightRef.current) return; | |||
| exportInFlightRef.current = true; | |||
| setExporting(true); | |||
| setSaveError(null); | |||
| try { | |||
| const code = (detail?.itemCode ?? currentBom?.code ?? "BOM").trim() || "BOM"; | |||
| const rev = detail?.revisionNo ?? 1; | |||
| const { blob, fileName } = await downloadBomDetailExcelClient( | |||
| versionId, | |||
| `${code}_V${rev}.xlsx`, | |||
| ); | |||
| const url = URL.createObjectURL(blob); | |||
| const a = document.createElement("a"); | |||
| a.href = url; | |||
| a.download = fileName; | |||
| a.click(); | |||
| URL.revokeObjectURL(url); | |||
| } catch (e: unknown) { | |||
| setSaveError(e instanceof Error ? e.message : t("Export excel failed")); | |||
| } finally { | |||
| setExporting(false); | |||
| exportInFlightRef.current = false; | |||
| } | |||
| }, [currentBom?.code, detail?.itemCode, detail?.revisionNo, t, versionId]); | |||
| const handleToggleCompare = useCallback(async () => { | |||
| if (compareMode) { | |||
| setCompareMode(false); | |||
| @@ -866,7 +895,7 @@ const ImportBomDetailTab: React.FC = () => { | |||
| </Paper> | |||
| )} | |||
| {detail && ( | |||
| <Stack spacing={2} sx={{ pointerEvents: saving ? "none" : "auto" }}> | |||
| <Stack spacing={2} sx={{ pointerEvents: saving || exporting ? "none" : "auto" }}> | |||
| <Typography variant="subtitle1"> | |||
| {detail.itemCode} {detail.itemName} | |||
| </Typography> | |||
| @@ -879,8 +908,10 @@ const ImportBomDetailTab: React.FC = () => { | |||
| compareMode={compareMode} | |||
| compareToId={compareToId} | |||
| saving={saving} | |||
| exporting={exporting} | |||
| versionsRefreshKey={versionsRefreshKey} | |||
| saveDisabled={effectiveSaveDisabled} | |||
| exportDisabled={typeof versionId !== "number"} | |||
| saveError={ | |||
| blockedByUnresolvedMaterialIssue | |||
| ? unresolvedMaterialIssueMessage | |||
| @@ -892,6 +923,7 @@ const ImportBomDetailTab: React.FC = () => { | |||
| }} | |||
| onToggleCompare={() => void handleToggleCompare()} | |||
| onCompareToChange={setCompareToId} | |||
| onExport={() => void handleExportExcel()} | |||
| onSave={() => void handleSave()} | |||
| /> | |||
| )} | |||
| @@ -42,6 +42,8 @@ | |||
| "Download corrected excel": "Download corrected Excel", | |||
| "Downloading...": "Downloading…", | |||
| "Download corrected excel failed": "Failed to download corrected Excel", | |||
| "Export Excel": "Export Excel", | |||
| "Export excel failed": "Failed to export Excel", | |||
| "Confirm import": "Confirm import", | |||
| "Download issue log": "Download issue log Excel", | |||
| "Import completed": "Import completed", | |||
| @@ -42,6 +42,8 @@ | |||
| "Download corrected excel": "下載修正版 Excel", | |||
| "Downloading...": "下載中…", | |||
| "Download corrected excel failed": "下載修正版 Excel 失敗", | |||
| "Export Excel": "匯出 Excel", | |||
| "Export excel failed": "匯出 Excel 失敗", | |||
| "Confirm import": "確認匯入", | |||
| "Download issue log": "下載檢查結果 Excel", | |||
| "Import completed": "匯入完成", | |||