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}
+ />
+
+
+
+ >
+ );
+};
+
+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";