From 9ead9d244e65062f5febf53b74f010294995313e Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 2 Mar 2026 09:53:10 +0800 Subject: [PATCH] Bom Supporting Function --- src/app/(main)/settings/bomWeighting/page.tsx | 25 ++ src/app/api/settings/bomWeighting/actions.ts | 30 +++ src/app/api/settings/bomWeighting/client.ts | 23 ++ src/app/api/settings/bomWeighting/index.ts | 23 ++ src/app/api/settings/bomWeighting/page.tsx | 25 ++ .../BomWeightingScoreTable.tsx | 231 ++++++++++++++++++ .../BomWeightingScoreTable/index.ts | 1 + 7 files changed, 358 insertions(+) create mode 100644 src/app/(main)/settings/bomWeighting/page.tsx create mode 100644 src/app/api/settings/bomWeighting/actions.ts create mode 100644 src/app/api/settings/bomWeighting/client.ts create mode 100644 src/app/api/settings/bomWeighting/index.ts create mode 100644 src/app/api/settings/bomWeighting/page.tsx create mode 100644 src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx create mode 100644 src/components/BomWeightingScoreTable/index.ts diff --git a/src/app/(main)/settings/bomWeighting/page.tsx b/src/app/(main)/settings/bomWeighting/page.tsx new file mode 100644 index 0000000..4456c5f --- /dev/null +++ b/src/app/(main)/settings/bomWeighting/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import PageTitleBar from "@/components/PageTitleBar"; +import BomWeightingScoreTable from "@/components/BomWeightingScoreTable"; +import { fetchBomWeightingScores } from "@/app/api/settings/bomWeighting"; + +export const metadata: Metadata = { + title: "BOM Weighting Score", +}; + +const BomWeightingScorePage: React.FC = async () => { + const { t } = await getServerI18n("common"); + const bomWeightingScores = await fetchBomWeightingScores(); + + return ( + <> + + + + + + ); +}; + +export default BomWeightingScorePage; diff --git a/src/app/api/settings/bomWeighting/actions.ts b/src/app/api/settings/bomWeighting/actions.ts new file mode 100644 index 0000000..a33c011 --- /dev/null +++ b/src/app/api/settings/bomWeighting/actions.ts @@ -0,0 +1,30 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { BomWeightingScoreResult } from "."; + +export interface UpdateBomWeightingScoreInputs { + id: number; + name: string; + range: number; + weighting: number; + remarks?: string; +} + +export const updateBomWeightingScore = async (data: UpdateBomWeightingScoreInputs) => { + const response = await serverFetchJson( + `${BASE_API_URL}/bomWeightingScores/${data.id}`, + { + method: "PUT", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + revalidateTag("bomWeightingScores"); + revalidatePath("/(main)/settings/bomWeighting"); + + return response; +}; diff --git a/src/app/api/settings/bomWeighting/client.ts b/src/app/api/settings/bomWeighting/client.ts new file mode 100644 index 0000000..a502763 --- /dev/null +++ b/src/app/api/settings/bomWeighting/client.ts @@ -0,0 +1,23 @@ +"use client"; + +import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { BomWeightingScoreResult } from "./index"; + +export interface UpdateBomWeightingScoreInputs { + id: number; + name: string; + range: number; + weighting: number; + remarks?: string; +} + +export const updateBomWeightingScoreClient = async ( + data: UpdateBomWeightingScoreInputs +): Promise => { + const response = await axiosInstance.put( + `${NEXT_PUBLIC_API_URL}/bomWeightingScores/${data.id}`, + data + ); + return response.data; +}; diff --git a/src/app/api/settings/bomWeighting/index.ts b/src/app/api/settings/bomWeighting/index.ts new file mode 100644 index 0000000..2ba5b8f --- /dev/null +++ b/src/app/api/settings/bomWeighting/index.ts @@ -0,0 +1,23 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface BomWeightingScoreResult { + id: number; + code: string; + name: string; + range: number; + weighting: number | string | { value?: number; [key: string]: any }; + remarks?: string; +} + +export const preloadBomWeightingScores = () => { + fetchBomWeightingScores(); +}; + +export const fetchBomWeightingScores = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/bomWeightingScores`, { + next: { tags: ["bomWeightingScores"] }, + }); +}); diff --git a/src/app/api/settings/bomWeighting/page.tsx b/src/app/api/settings/bomWeighting/page.tsx new file mode 100644 index 0000000..4456c5f --- /dev/null +++ b/src/app/api/settings/bomWeighting/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import PageTitleBar from "@/components/PageTitleBar"; +import BomWeightingScoreTable from "@/components/BomWeightingScoreTable"; +import { fetchBomWeightingScores } from "@/app/api/settings/bomWeighting"; + +export const metadata: Metadata = { + title: "BOM Weighting Score", +}; + +const BomWeightingScorePage: React.FC = async () => { + const { t } = await getServerI18n("common"); + const bomWeightingScores = await fetchBomWeightingScores(); + + return ( + <> + + + + + + ); +}; + +export default BomWeightingScorePage; diff --git a/src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx b/src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx new file mode 100644 index 0000000..d6d9365 --- /dev/null +++ b/src/components/BomWeightingScoreTable/BomWeightingScoreTable.tsx @@ -0,0 +1,231 @@ +"use client"; + +import React, { useMemo, useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { BomWeightingScoreResult } from "@/app/api/settings/bomWeighting"; +import { updateBomWeightingScoreClient } from "@/app/api/settings/bomWeighting/client"; +import { GridColDef, GridValueGetterParams, GridValueFormatterParams, GridRenderCellParams } from "@mui/x-data-grid"; +import StyledDataGrid from "../StyledDataGrid"; +import Paper from "@mui/material/Paper"; +import IconButton from "@mui/material/IconButton"; +import EditNote from "@mui/icons-material/EditNote"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; +import { successDialog } from "../Swal/CustomAlerts"; + +interface Props { + bomWeightingScores: BomWeightingScoreResult[]; +} + +const BomWeightingScoreTable: React.FC & { Loading?: React.FC } = ({ bomWeightingScores: initialBomWeightingScores }) => { + const { t } = useTranslation("common"); + const [bomWeightingScores, setBomWeightingScores] = useState(initialBomWeightingScores); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [formData, setFormData] = useState({ + name: "", + range: "", + weighting: "", + remarks: "", + }); + + useEffect(() => { + setBomWeightingScores(initialBomWeightingScores); + }, [initialBomWeightingScores]); + + const handleEditClick = useCallback((row: BomWeightingScoreResult) => { + let weightingValue = 0; + if (row.weighting != null) { + if (typeof row.weighting === "object" && row.weighting !== null) { + const obj = row.weighting as any; + weightingValue = typeof obj.value === "number" ? obj.value : parseFloat(String(row.weighting)) || 0; + } else { + weightingValue = typeof row.weighting === "number" ? row.weighting : parseFloat(String(row.weighting)) || 0; + } + } + + setEditingItem(row); + setFormData({ + name: row.name || "", + range: String(row.range || ""), + weighting: String(weightingValue), + remarks: row.remarks || "", + }); + setEditDialogOpen(true); + }, []); + + const handleCloseDialog = useCallback(() => { + setEditDialogOpen(false); + setEditingItem(null); + setFormData({ name: "", range: "", weighting: "", remarks: "" }); + }, []); + + const handleSave = useCallback(async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + if (!editingItem || isSaving) return; + + setIsSaving(true); + try { + const updated = await updateBomWeightingScoreClient({ + id: editingItem.id, + name: editingItem.name, // Keep original name + range: parseInt(formData.range, 10), + weighting: parseFloat(formData.weighting), + remarks: editingItem.remarks || undefined, // Keep original remarks + }); + + // Update local state immediately + setBomWeightingScores((prev) => + prev.map((item) => (item.id === editingItem.id ? updated : item)) + ); + + // Close dialog first, then show success message + handleCloseDialog(); + await successDialog(t("Update Success"), t); + } catch (error: any) { + console.error("Error updating bom weighting score:", error); + // Show error message to user + const errorMessage = error?.response?.data?.message || error?.message || t("Update Failed") || "Update failed. Please try again."; + alert(errorMessage); + } finally { + setIsSaving(false); + } + }, [editingItem, formData, t, handleCloseDialog, isSaving]); + + const columns = useMemo[]>( + () => [ + { + field: "actions", + headerName: t("Edit"), + width: 100, + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + handleEditClick(params.row)} + color="primary" + > + + + ), + }, + { + field: "name", + headerName: t("Name"), + flex: 1, + }, + { + field: "range", + headerName: t("Range"), + flex: 1, + align: "left", + headerAlign: "left", + }, + { + field: "weighting", + headerName: t("Weighting"), + flex: 1, + valueGetter: (params: GridValueGetterParams) => { + const weighting = params.row.weighting; + if (weighting == null || weighting === undefined) return null; + + if (typeof weighting === "object" && weighting !== null) { + const obj = weighting as any; + if (typeof obj.value === "number") { + return obj.value; + } + if (typeof obj.toString === "function") { + return parseFloat(obj.toString()); + } + const numValue = parseFloat(String(weighting)); + return isNaN(numValue) ? null : numValue; + } + + const numValue = typeof weighting === "number" ? weighting : parseFloat(String(weighting)); + return isNaN(numValue) ? null : numValue; + }, + valueFormatter: (params: GridValueFormatterParams) => { + const value = params.value; + if (value == null || value === undefined) return ""; + return typeof value === "number" ? value.toFixed(2) : ""; + }, + }, + ], + [t, handleEditClick], + ); + + return ( + <> + + row.id} + autoHeight + disableRowSelectionOnClick + hideFooterPagination={true} + /> + + + + {t("Edit BOM Weighting Score")} + + + setFormData({ ...formData, range: e.target.value })} + margin="normal" + required + /> + setFormData({ ...formData, weighting: e.target.value })} + margin="normal" + required + /> + + + + + + + + ); +}; + +BomWeightingScoreTable.Loading = () => { + return ( + +
Loading...
+
+ ); +}; + +export default BomWeightingScoreTable; diff --git a/src/components/BomWeightingScoreTable/index.ts b/src/components/BomWeightingScoreTable/index.ts new file mode 100644 index 0000000..1168827 --- /dev/null +++ b/src/components/BomWeightingScoreTable/index.ts @@ -0,0 +1 @@ +export { default } from "./BomWeightingScoreTable";