(`${BASE_API_URL}/qcItemAll/itemByCode/${encodeURIComponent(code)}`);
+ } catch (error) {
+ // Item not found
+ return null;
+ }
+};
+
+
+
diff --git a/src/app/api/settings/qcItemAll/index.ts b/src/app/api/settings/qcItemAll/index.ts
new file mode 100644
index 0000000..e228af6
--- /dev/null
+++ b/src/app/api/settings/qcItemAll/index.ts
@@ -0,0 +1,101 @@
+// Type definitions that can be used in both client and server components
+export interface ItemQcCategoryMappingInfo {
+ id: number;
+ itemId: number;
+ itemCode?: string;
+ itemName?: string;
+ qcCategoryId: number;
+ qcCategoryCode?: string;
+ qcCategoryName?: string;
+ type?: string;
+}
+
+export interface QcItemInfo {
+ id: number;
+ order: number;
+ qcItemId: number;
+ code: string;
+ name?: string;
+ description?: string;
+}
+
+export interface DeleteResponse {
+ success: boolean;
+ message?: string;
+ canDelete: boolean;
+}
+
+export interface QcCategoryWithCounts {
+ id: number;
+ code: string;
+ name: string;
+ description?: string;
+ itemCount: number;
+ qcItemCount: number;
+}
+
+export interface QcCategoryWithItemCount {
+ id: number;
+ code: string;
+ name: string;
+ description?: string;
+ itemCount: number;
+}
+
+export interface QcCategoryWithQcItemCount {
+ id: number;
+ code: string;
+ name: string;
+ description?: string;
+ qcItemCount: number;
+}
+
+export interface QcItemWithCounts {
+ id: number;
+ code: string;
+ name: string;
+ description?: string;
+ qcCategoryCount: number;
+}
+
+// Type definitions that match the server-only types
+export interface QcCategoryResult {
+ id: number;
+ code: string;
+ name: string;
+ description?: string;
+}
+
+export interface QcItemResult {
+ id: number;
+ code: string;
+ name: string;
+ description: string;
+}
+
+export interface ItemsResult {
+ id: string | number;
+ code: string;
+ name: string;
+ description: string | undefined;
+ remarks: string | undefined;
+ shelfLife: number | undefined;
+ countryOfOrigin: string | undefined;
+ maxQty: number | undefined;
+ type: string;
+ qcChecks: any[];
+ action?: any;
+ fgName?: string;
+ excludeDate?: string;
+ qcCategory?: QcCategoryResult;
+ store_id?: string | undefined;
+ warehouse?: string | undefined;
+ area?: string | undefined;
+ slot?: string | undefined;
+ LocationCode?: string | undefined;
+ locationCode?: string | undefined;
+ isEgg?: boolean | undefined;
+ isFee?: boolean | undefined;
+ isBag?: boolean | undefined;
+}
+
diff --git a/src/components/QcItemAll/QcItemAllTabs.tsx b/src/components/QcItemAll/QcItemAllTabs.tsx
new file mode 100644
index 0000000..47ed9b5
--- /dev/null
+++ b/src/components/QcItemAll/QcItemAllTabs.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useState, ReactNode, useEffect } from "react";
+import { Box, Tabs, Tab } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useSearchParams, useRouter } from "next/navigation";
+
+interface TabPanelProps {
+ children?: ReactNode;
+ index: number;
+ value: number;
+}
+
+function TabPanel(props: TabPanelProps) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && {children}}
+
+ );
+}
+
+interface QcItemAllTabsProps {
+ tab0Content: ReactNode;
+ tab1Content: ReactNode;
+ tab2Content: ReactNode;
+ tab3Content: ReactNode;
+}
+
+const QcItemAllTabs: React.FC = ({
+ tab0Content,
+ tab1Content,
+ tab2Content,
+ tab3Content,
+}) => {
+ const { t } = useTranslation("qcItemAll");
+ const searchParams = useSearchParams();
+ const router = useRouter();
+
+ const getInitialTab = () => {
+ const tab = searchParams.get("tab");
+ if (tab === "1") return 1;
+ if (tab === "2") return 2;
+ if (tab === "3") return 3;
+ return 0;
+ };
+
+ const [currentTab, setCurrentTab] = useState(getInitialTab);
+
+ useEffect(() => {
+ const tab = searchParams.get("tab");
+ const tabIndex = tab ? parseInt(tab, 10) : 0;
+ setCurrentTab(tabIndex);
+ }, [searchParams]);
+
+ const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
+ setCurrentTab(newValue);
+ const params = new URLSearchParams(searchParams.toString());
+ if (newValue === 0) {
+ params.delete("tab");
+ } else {
+ params.set("tab", newValue.toString());
+ }
+ router.push(`?${params.toString()}`, { scroll: false });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {tab0Content}
+
+
+
+ {tab1Content}
+
+
+
+ {tab2Content}
+
+
+
+ {tab3Content}
+
+
+ );
+};
+
+export default QcItemAllTabs;
+
diff --git a/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx b/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx
new file mode 100644
index 0000000..585480d
--- /dev/null
+++ b/src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx
@@ -0,0 +1,351 @@
+"use client";
+
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Grid,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+ IconButton,
+ CircularProgress,
+} from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { Add, Delete, Edit } from "@mui/icons-material";
+import SearchBox, { Criterion } from "../SearchBox/SearchBox";
+import SearchResults, { Column } from "../SearchResults/SearchResults";
+import {
+ saveItemQcCategoryMapping,
+ deleteItemQcCategoryMapping,
+ getItemQcCategoryMappings,
+ fetchQcCategoriesForAll,
+ fetchItemsForAll,
+ getItemByCode,
+} from "@/app/api/settings/qcItemAll/actions";
+import {
+ QcCategoryResult,
+ ItemsResult,
+} from "@/app/api/settings/qcItemAll";
+import { ItemQcCategoryMappingInfo } from "@/app/api/settings/qcItemAll";
+import {
+ deleteDialog,
+ errorDialogWithContent,
+ submitDialog,
+ successDialog,
+} from "../Swal/CustomAlerts";
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const Tab0ItemQcCategoryMapping: React.FC = () => {
+ const { t } = useTranslation("qcItemAll");
+ const [qcCategories, setQcCategories] = useState([]);
+ const [filteredQcCategories, setFilteredQcCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [mappings, setMappings] = useState([]);
+ const [openDialog, setOpenDialog] = useState(false);
+ const [openAddDialog, setOpenAddDialog] = useState(false);
+ const [itemCode, setItemCode] = useState("");
+ const [validatedItem, setValidatedItem] = useState(null);
+ const [itemCodeError, setItemCodeError] = useState("");
+ const [validatingItemCode, setValidatingItemCode] = useState(false);
+ const [selectedType, setSelectedType] = useState("IQC");
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ // Only load categories list (same as Tab 2) - fast!
+ const categories = await fetchQcCategoriesForAll();
+ setQcCategories(categories || []);
+ setFilteredQcCategories(categories || []);
+ } catch (error) {
+ console.error("Tab0: Error loading data:", error);
+ setQcCategories([]);
+ setFilteredQcCategories([]);
+ if (error instanceof Error) {
+ errorDialogWithContent(t("Error"), error.message, t);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadData();
+ }, []);
+
+ const handleViewMappings = useCallback(async (category: QcCategoryResult) => {
+ setSelectedCategory(category);
+ const mappingData = await getItemQcCategoryMappings(category.id);
+ setMappings(mappingData);
+ setOpenDialog(true);
+ }, []);
+
+ const handleAddMapping = useCallback(() => {
+ if (!selectedCategory) return;
+ setItemCode("");
+ setValidatedItem(null);
+ setItemCodeError("");
+ setOpenAddDialog(true);
+ }, [selectedCategory]);
+
+ const handleItemCodeChange = useCallback(async (code: string) => {
+ setItemCode(code);
+ setValidatedItem(null);
+ setItemCodeError("");
+
+ if (!code || code.trim() === "") {
+ return;
+ }
+
+ setValidatingItemCode(true);
+ try {
+ const item = await getItemByCode(code.trim());
+ if (item) {
+ setValidatedItem(item);
+ setItemCodeError("");
+ } else {
+ setValidatedItem(null);
+ setItemCodeError(t("Item code not found"));
+ }
+ } catch (error) {
+ setValidatedItem(null);
+ setItemCodeError(t("Error validating item code"));
+ } finally {
+ setValidatingItemCode(false);
+ }
+ }, [t]);
+
+ const handleSaveMapping = useCallback(async () => {
+ if (!selectedCategory || !validatedItem) return;
+
+ await submitDialog(async () => {
+ try {
+ await saveItemQcCategoryMapping(
+ validatedItem.id as number,
+ selectedCategory.id,
+ selectedType
+ );
+ // Close add dialog first
+ setOpenAddDialog(false);
+ setItemCode("");
+ setValidatedItem(null);
+ setItemCodeError("");
+ // Reload mappings to update the view
+ const mappingData = await getItemQcCategoryMappings(selectedCategory.id);
+ setMappings(mappingData);
+ // Show success message after closing dialogs
+ await successDialog(t("Submit Success"), t);
+ // Keep the view dialog open to show updated data
+ } catch (error) {
+ errorDialogWithContent(t("Submit Error"), String(error), t);
+ }
+ }, t);
+ }, [selectedCategory, validatedItem, selectedType, t]);
+
+ const handleDeleteMapping = useCallback(
+ async (mappingId: number) => {
+ if (!selectedCategory) return;
+
+ deleteDialog(async () => {
+ try {
+ await deleteItemQcCategoryMapping(mappingId);
+ await successDialog(t("Delete Success"), t);
+ // Reload mappings
+ const mappingData = await getItemQcCategoryMappings(selectedCategory.id);
+ setMappings(mappingData);
+ // No need to reload categories list - it doesn't change
+ } catch (error) {
+ errorDialogWithContent(t("Delete Error"), String(error), t);
+ }
+ }, t);
+ },
+ [selectedCategory, t]
+ );
+
+ const typeOptions = ["IQC", "IPQC", "OQC", "FQC"];
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Code"), paramName: "code", type: "text" },
+ { label: t("Name"), paramName: "name", type: "text" },
+ ],
+ [t]
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredQcCategories(qcCategories);
+ }, [qcCategories]);
+
+ const columnWidthSx = (width = "10%") => {
+ return { width: width, whiteSpace: "nowrap" };
+ };
+
+ const columns = useMemo[]>(
+ () => [
+ { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") },
+ { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") },
+ {
+ name: "id",
+ label: t("Actions"),
+ onClick: (category) => handleViewMappings(category),
+ buttonIcon: ,
+ buttonIcons: {} as any,
+ sx: columnWidthSx("10%"),
+ },
+ ],
+ [t, handleViewMappings]
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {
+ setFilteredQcCategories(
+ qcCategories.filter(
+ (qc) =>
+ (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
+ (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
+ )
+ );
+ }}
+ onReset={onReset}
+ />
+
+ items={filteredQcCategories}
+ columns={columns}
+ />
+
+ {/* View Mappings Dialog */}
+
+
+ {/* Add Mapping Dialog */}
+
+
+ );
+};
+
+export default Tab0ItemQcCategoryMapping;
+
diff --git a/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx b/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx
new file mode 100644
index 0000000..5544dfb
--- /dev/null
+++ b/src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx
@@ -0,0 +1,304 @@
+"use client";
+
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Autocomplete,
+ CircularProgress,
+} from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { Add, Delete, Edit } from "@mui/icons-material";
+import SearchBox, { Criterion } from "../SearchBox/SearchBox";
+import SearchResults, { Column } from "../SearchResults/SearchResults";
+import {
+ saveQcCategoryQcItemMapping,
+ deleteQcCategoryQcItemMapping,
+ getQcCategoryQcItemMappings,
+ fetchQcCategoriesForAll,
+ fetchQcItemsForAll,
+} from "@/app/api/settings/qcItemAll/actions";
+import {
+ QcCategoryResult,
+ QcItemResult,
+} from "@/app/api/settings/qcItemAll";
+import { QcItemInfo } from "@/app/api/settings/qcItemAll";
+import {
+ deleteDialog,
+ errorDialogWithContent,
+ submitDialog,
+ successDialog,
+} from "../Swal/CustomAlerts";
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const Tab1QcCategoryQcItemMapping: React.FC = () => {
+ const { t } = useTranslation("qcItemAll");
+ const [qcCategories, setQcCategories] = useState([]);
+ const [filteredQcCategories, setFilteredQcCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [mappings, setMappings] = useState([]);
+ const [openDialog, setOpenDialog] = useState(false);
+ const [openAddDialog, setOpenAddDialog] = useState(false);
+ const [qcItems, setQcItems] = useState([]);
+ const [selectedQcItem, setSelectedQcItem] = useState(null);
+ const [order, setOrder] = useState(0);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ // Only load categories list (same as Tab 2) - fast!
+ const categories = await fetchQcCategoriesForAll();
+ setQcCategories(categories || []);
+ setFilteredQcCategories(categories || []);
+ } catch (error) {
+ console.error("Error loading data:", error);
+ setQcCategories([]); // Ensure it's always an array
+ setFilteredQcCategories([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadData();
+ }, []);
+
+ const handleViewMappings = useCallback(async (category: QcCategoryResult) => {
+ setSelectedCategory(category);
+ // Load mappings when user clicks View (lazy loading)
+ const mappingData = await getQcCategoryQcItemMappings(category.id);
+ setMappings(mappingData);
+ setOpenDialog(true);
+ }, []);
+
+ const handleAddMapping = useCallback(async () => {
+ if (!selectedCategory) return;
+ // Load qc items list when opening add dialog
+ try {
+ const itemsData = await fetchQcItemsForAll();
+ setQcItems(itemsData);
+ } catch (error) {
+ console.error("Error loading qc items:", error);
+ }
+ setOpenAddDialog(true);
+ setOrder(0);
+ setSelectedQcItem(null);
+ }, [selectedCategory]);
+
+ const handleSaveMapping = useCallback(async () => {
+ if (!selectedCategory || !selectedQcItem) return;
+
+ await submitDialog(async () => {
+ try {
+ await saveQcCategoryQcItemMapping(
+ selectedCategory.id,
+ selectedQcItem.id,
+ order,
+ undefined // No description needed - qcItem already has description
+ );
+ // Close add dialog first
+ setOpenAddDialog(false);
+ setSelectedQcItem(null);
+ setOrder(0);
+ // Reload mappings to update the view
+ const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id);
+ setMappings(mappingData);
+ // Show success message after closing dialogs
+ await successDialog(t("Submit Success"), t);
+ // Keep the view dialog open to show updated data
+ } catch (error) {
+ errorDialogWithContent(t("Submit Error"), String(error), t);
+ }
+ }, t);
+ }, [selectedCategory, selectedQcItem, order, t]);
+
+ const handleDeleteMapping = useCallback(
+ async (mappingId: number) => {
+ if (!selectedCategory) return;
+
+ deleteDialog(async () => {
+ try {
+ await deleteQcCategoryQcItemMapping(mappingId);
+ await successDialog(t("Delete Success"), t);
+ // Reload mappings
+ const mappingData = await getQcCategoryQcItemMappings(selectedCategory.id);
+ setMappings(mappingData);
+ // No need to reload categories list - it doesn't change
+ } catch (error) {
+ errorDialogWithContent(t("Delete Error"), String(error), t);
+ }
+ }, t);
+ },
+ [selectedCategory, t]
+ );
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Code"), paramName: "code", type: "text" },
+ { label: t("Name"), paramName: "name", type: "text" },
+ ],
+ [t]
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredQcCategories(qcCategories);
+ }, [qcCategories]);
+
+ const columnWidthSx = (width = "10%") => {
+ return { width: width, whiteSpace: "nowrap" };
+ };
+
+ const columns = useMemo[]>(
+ () => [
+ { name: "code", label: t("Qc Category Code"), sx: columnWidthSx("20%") },
+ { name: "name", label: t("Qc Category Name"), sx: columnWidthSx("40%") },
+ {
+ name: "id",
+ label: t("Actions"),
+ onClick: (category) => handleViewMappings(category),
+ buttonIcon: ,
+ buttonIcons: {} as any,
+ sx: columnWidthSx("10%"),
+ },
+ ],
+ [t, handleViewMappings]
+ );
+
+ return (
+
+ {
+ setFilteredQcCategories(
+ qcCategories.filter(
+ (qc) =>
+ (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
+ (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
+ )
+ );
+ }}
+ onReset={onReset}
+ />
+
+ items={filteredQcCategories}
+ columns={columns}
+ />
+
+ {/* View Mappings Dialog */}
+
+
+ {/* Add Mapping Dialog */}
+
+
+ );
+};
+
+export default Tab1QcCategoryQcItemMapping;
+
diff --git a/src/components/QcItemAll/Tab2QcCategoryManagement.tsx b/src/components/QcItemAll/Tab2QcCategoryManagement.tsx
new file mode 100644
index 0000000..5e0992c
--- /dev/null
+++ b/src/components/QcItemAll/Tab2QcCategoryManagement.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import SearchBox, { Criterion } from "../SearchBox";
+import { useTranslation } from "react-i18next";
+import SearchResults, { Column } from "../SearchResults";
+import EditNote from "@mui/icons-material/EditNote";
+import { fetchQcCategoriesForAll } from "@/app/api/settings/qcItemAll/actions";
+import { QcCategoryResult } from "@/app/api/settings/qcItemAll";
+import {
+ deleteDialog,
+ errorDialogWithContent,
+ submitDialog,
+ successDialog,
+} from "../Swal/CustomAlerts";
+import {
+ deleteQcCategoryWithValidation,
+ canDeleteQcCategory,
+ saveQcCategoryWithValidation,
+ SaveQcCategoryInputs,
+} from "@/app/api/settings/qcItemAll/actions";
+import Delete from "@mui/icons-material/Delete";
+import { Add } from "@mui/icons-material";
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material";
+import QcCategoryDetails from "../QcCategorySave/QcCategoryDetails";
+import { FormProvider, useForm } from "react-hook-form";
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const Tab2QcCategoryManagement: React.FC = () => {
+ const { t } = useTranslation("qcItemAll");
+ const [qcCategories, setQcCategories] = useState([]);
+ const [filteredQcCategories, setFilteredQcCategories] = useState([]);
+ const [openDialog, setOpenDialog] = useState(false);
+ const [editingCategory, setEditingCategory] = useState(null);
+
+ useEffect(() => {
+ loadCategories();
+ }, []);
+
+ const loadCategories = async () => {
+ const categories = await fetchQcCategoriesForAll();
+ setQcCategories(categories);
+ setFilteredQcCategories(categories);
+ };
+
+ const formProps = useForm({
+ defaultValues: {
+ code: "",
+ name: "",
+ description: "",
+ },
+ });
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Code"), paramName: "code", type: "text" },
+ { label: t("Name"), paramName: "name", type: "text" },
+ ],
+ [t]
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredQcCategories(qcCategories);
+ }, [qcCategories]);
+
+ const handleEdit = useCallback((qcCategory: QcCategoryResult) => {
+ setEditingCategory(qcCategory);
+ formProps.reset({
+ id: qcCategory.id,
+ code: qcCategory.code,
+ name: qcCategory.name,
+ description: qcCategory.description || "",
+ });
+ setOpenDialog(true);
+ }, [formProps]);
+
+ const handleAdd = useCallback(() => {
+ setEditingCategory(null);
+ formProps.reset({
+ code: "",
+ name: "",
+ description: "",
+ });
+ setOpenDialog(true);
+ }, [formProps]);
+
+ const handleSubmit = useCallback(async (data: SaveQcCategoryInputs) => {
+ await submitDialog(async () => {
+ try {
+ const response = await saveQcCategoryWithValidation(data);
+ if (response.errors) {
+ let errorContents = "";
+ for (const [key, value] of Object.entries(response.errors)) {
+ formProps.setError(key as keyof SaveQcCategoryInputs, {
+ type: "custom",
+ message: value,
+ });
+ errorContents = errorContents + t(value) + "
";
+ }
+ errorDialogWithContent(t("Submit Error"), errorContents, t);
+ } else {
+ await successDialog(t("Submit Success"), t);
+ setOpenDialog(false);
+ await loadCategories();
+ }
+ } catch (error) {
+ errorDialogWithContent(t("Submit Error"), String(error), t);
+ }
+ }, t);
+ }, [formProps, t]);
+
+ const handleDelete = useCallback(async (qcCategory: QcCategoryResult) => {
+ // Check if can delete first
+ const canDelete = await canDeleteQcCategory(qcCategory.id); // This is a server action, token handled server-side
+
+ if (!canDelete) {
+ errorDialogWithContent(
+ t("Cannot Delete"),
+ t("Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.").replace("{itemCount}", "some").replace("{qcItemCount}", "some"),
+ t
+ );
+ return;
+ }
+
+ deleteDialog(async () => {
+ try {
+ const response = await deleteQcCategoryWithValidation(qcCategory.id);
+ if (!response.success || !response.canDelete) {
+ errorDialogWithContent(
+ t("Delete Error"),
+ response.message || t("Cannot Delete"),
+ t
+ );
+ } else {
+ await successDialog(t("Delete Success"), t);
+ await loadCategories();
+ }
+ } catch (error) {
+ errorDialogWithContent(t("Delete Error"), String(error), t);
+ }
+ }, t);
+ }, [t]);
+
+ const columnWidthSx = (width = "10%") => {
+ return { width: width, whiteSpace: "nowrap" };
+ };
+
+ const columns = useMemo[]>(
+ () => [
+ {
+ name: "id",
+ label: t("Details"),
+ onClick: handleEdit,
+ buttonIcon: ,
+ sx: columnWidthSx("5%"),
+ },
+ { name: "code", label: t("Code"), sx: columnWidthSx("15%") },
+ { name: "name", label: t("Name"), sx: columnWidthSx("30%") },
+ {
+ name: "id",
+ label: t("Delete"),
+ onClick: handleDelete,
+ buttonIcon: ,
+ buttonColor: "error",
+ sx: columnWidthSx("5%"),
+ },
+ ],
+ [t, handleEdit, handleDelete]
+ );
+
+ return (
+ <>
+
+ }
+ onClick={handleAdd}
+ >
+ {t("Create Qc Category")}
+
+
+ {
+ setFilteredQcCategories(
+ qcCategories.filter(
+ (qc) =>
+ (!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
+ (!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
+ )
+ );
+ }}
+ onReset={onReset}
+ />
+
+ items={filteredQcCategories}
+ columns={columns}
+ />
+
+ {/* Add/Edit Dialog */}
+
+ >
+ );
+};
+
+export default Tab2QcCategoryManagement;
+
diff --git a/src/components/QcItemAll/Tab3QcItemManagement.tsx b/src/components/QcItemAll/Tab3QcItemManagement.tsx
new file mode 100644
index 0000000..33591fc
--- /dev/null
+++ b/src/components/QcItemAll/Tab3QcItemManagement.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import SearchBox, { Criterion } from "../SearchBox";
+import { useTranslation } from "react-i18next";
+import SearchResults, { Column } from "../SearchResults";
+import EditNote from "@mui/icons-material/EditNote";
+import { fetchQcItemsForAll } from "@/app/api/settings/qcItemAll/actions";
+import { QcItemResult } from "@/app/api/settings/qcItemAll";
+import {
+ deleteDialog,
+ errorDialogWithContent,
+ submitDialog,
+ successDialog,
+} from "../Swal/CustomAlerts";
+import {
+ deleteQcItemWithValidation,
+ canDeleteQcItem,
+ saveQcItemWithValidation,
+ SaveQcItemInputs,
+} from "@/app/api/settings/qcItemAll/actions";
+import Delete from "@mui/icons-material/Delete";
+import { Add } from "@mui/icons-material";
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack } from "@mui/material";
+import QcItemDetails from "../QcItemSave/QcItemDetails";
+import { FormProvider, useForm } from "react-hook-form";
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const Tab3QcItemManagement: React.FC = () => {
+ const { t } = useTranslation("qcItemAll");
+ const [qcItems, setQcItems] = useState([]);
+ const [filteredQcItems, setFilteredQcItems] = useState([]);
+ const [openDialog, setOpenDialog] = useState(false);
+ const [editingItem, setEditingItem] = useState(null);
+
+ useEffect(() => {
+ loadItems();
+ }, []);
+
+ const loadItems = async () => {
+ const items = await fetchQcItemsForAll();
+ setQcItems(items);
+ setFilteredQcItems(items);
+ };
+
+ const formProps = useForm({
+ defaultValues: {
+ code: "",
+ name: "",
+ description: "",
+ },
+ });
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ { label: t("Code"), paramName: "code", type: "text" },
+ { label: t("Name"), paramName: "name", type: "text" },
+ ],
+ [t]
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredQcItems(qcItems);
+ }, [qcItems]);
+
+ const handleEdit = useCallback((qcItem: QcItemResult) => {
+ setEditingItem(qcItem);
+ formProps.reset({
+ id: qcItem.id,
+ code: qcItem.code,
+ name: qcItem.name,
+ description: qcItem.description || "",
+ });
+ setOpenDialog(true);
+ }, [formProps]);
+
+ const handleAdd = useCallback(() => {
+ setEditingItem(null);
+ formProps.reset({
+ code: "",
+ name: "",
+ description: "",
+ });
+ setOpenDialog(true);
+ }, [formProps]);
+
+ const handleSubmit = useCallback(async (data: SaveQcItemInputs) => {
+ await submitDialog(async () => {
+ try {
+ const response = await saveQcItemWithValidation(data);
+ if (response.errors) {
+ let errorContents = "";
+ for (const [key, value] of Object.entries(response.errors)) {
+ formProps.setError(key as keyof SaveQcItemInputs, {
+ type: "custom",
+ message: value,
+ });
+ errorContents = errorContents + t(value) + "
";
+ }
+ errorDialogWithContent(t("Submit Error"), errorContents, t);
+ } else {
+ await successDialog(t("Submit Success"), t);
+ setOpenDialog(false);
+ await loadItems();
+ }
+ } catch (error) {
+ errorDialogWithContent(t("Submit Error"), String(error), t);
+ }
+ }, t);
+ }, [formProps, t]);
+
+ const handleDelete = useCallback(async (qcItem: QcItemResult) => {
+ // Check if can delete first
+ const canDelete = await canDeleteQcItem(qcItem.id);
+
+ if (!canDelete) {
+ errorDialogWithContent(
+ t("Cannot Delete"),
+ t("Cannot delete QcItem. It is linked to one or more QcCategories."),
+ t
+ );
+ return;
+ }
+
+ deleteDialog(async () => {
+ try {
+ const response = await deleteQcItemWithValidation(qcItem.id);
+ if (!response.success || !response.canDelete) {
+ errorDialogWithContent(
+ t("Delete Error"),
+ response.message || t("Cannot Delete"),
+ t
+ );
+ } else {
+ await successDialog(t("Delete Success"), t);
+ await loadItems();
+ }
+ } catch (error) {
+ errorDialogWithContent(t("Delete Error"), String(error), t);
+ }
+ }, t);
+ }, [t]);
+
+ const columnWidthSx = (width = "10%") => {
+ return { width: width, whiteSpace: "nowrap" };
+ };
+
+ const columns = useMemo[]>(
+ () => [
+ {
+ name: "id",
+ label: t("Details"),
+ onClick: handleEdit,
+ buttonIcon: ,
+ sx: columnWidthSx("150px"),
+ },
+ { name: "code", label: t("Code"), sx: columnWidthSx() },
+ { name: "name", label: t("Name"), sx: columnWidthSx() },
+ { name: "description", label: t("Description") },
+ {
+ name: "id",
+ label: t("Delete"),
+ onClick: handleDelete,
+ buttonIcon: ,
+ buttonColor: "error",
+ },
+ ],
+ [t, handleEdit, handleDelete]
+ );
+
+ return (
+ <>
+
+ }
+ onClick={handleAdd}
+ >
+ {t("Create Qc Item")}
+
+
+ {
+ setFilteredQcItems(
+ qcItems.filter(
+ (qi) =>
+ (!query.code || qi.code.toLowerCase().includes(query.code.toLowerCase())) &&
+ (!query.name || qi.name.toLowerCase().includes(query.name.toLowerCase()))
+ )
+ );
+ }}
+ onReset={onReset}
+ />
+
+ items={filteredQcItems}
+ columns={columns}
+ />
+
+ {/* Add/Edit Dialog */}
+
+ >
+ );
+};
+
+export default Tab3QcItemManagement;
+
diff --git a/src/i18n/en/qcItemAll.json b/src/i18n/en/qcItemAll.json
new file mode 100644
index 0000000..f587f48
--- /dev/null
+++ b/src/i18n/en/qcItemAll.json
@@ -0,0 +1,58 @@
+{
+ "Qc Item All": "QC Management",
+ "Item and Qc Category Mapping": "Item and Qc Category Mapping",
+ "Qc Category and Qc Item Mapping": "Qc Category and Qc Item Mapping",
+ "Qc Category Management": "Qc Category Management",
+ "Qc Item Management": "Qc Item Management",
+ "Qc Category": "Qc Category",
+ "Qc Item": "Qc Item",
+ "Item": "Item",
+ "Code": "Code",
+ "Name": "Name",
+ "Description": "Description",
+ "Type": "Type",
+ "Order": "Order",
+ "Item Count": "Item Count",
+ "Qc Item Count": "Qc Item Count",
+ "Qc Category Count": "Qc Category Count",
+ "Actions": "Actions",
+ "View": "View",
+ "Edit": "Edit",
+ "Delete": "Delete",
+ "Add": "Add",
+ "Add Mapping": "Add Mapping",
+ "Add Association": "Add Association",
+ "Save": "Save",
+ "Cancel": "Cancel",
+ "Submit": "Submit",
+ "Details": "Details",
+ "Create Qc Category": "Create Qc Category",
+ "Edit Qc Category": "Edit Qc Category",
+ "Create Qc Item": "Create Qc Item",
+ "Edit Qc Item": "Edit Qc Item",
+ "Delete Success": "Delete Success",
+ "Delete Error": "Delete Error",
+ "Submit Success": "Submit Success",
+ "Submit Error": "Submit Error",
+ "Cannot Delete": "Cannot Delete",
+ "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.",
+ "Cannot delete QcItem. It is linked to one or more QcCategories.": "Cannot delete QcItem. It is linked to one or more QcCategories.",
+ "Select Item": "Select Item",
+ "Select Qc Category": "Select Qc Category",
+ "Select Qc Item": "Select Qc Item",
+ "Select Type": "Select Type",
+ "Item Code": "Item Code",
+ "Item Name": "Item Name",
+ "Qc Category Code": "Qc Category Code",
+ "Qc Category Name": "Qc Category Name",
+ "Qc Item Code": "Qc Item Code",
+ "Qc Item Name": "Qc Item Name",
+ "Mapping Details": "Mapping Details",
+ "Association Details": "Association Details",
+ "No mappings found": "No mappings found",
+ "No associations found": "No associations found",
+ "No data available": "No data available",
+ "Confirm Delete": "Confirm Delete",
+ "Are you sure you want to delete this item?": "Are you sure you want to delete this item?"
+}
+
diff --git a/src/i18n/zh/qcItemAll.json b/src/i18n/zh/qcItemAll.json
new file mode 100644
index 0000000..370113b
--- /dev/null
+++ b/src/i18n/zh/qcItemAll.json
@@ -0,0 +1,58 @@
+{
+ "Qc Item All": "QC 綜合管理",
+ "Item and Qc Category Mapping": "物料與品檢模板映射",
+ "Qc Category and Qc Item Mapping": "品檢模板與品檢項目映射",
+ "Qc Category Management": "品檢模板管理",
+ "Qc Item Management": "品檢項目管理",
+ "Qc Category": "品檢模板",
+ "Qc Item": "品檢項目",
+ "Item": "物料",
+ "Code": "編號",
+ "Name": "名稱",
+ "Description": "描述",
+ "Type": "類型",
+ "Order": "順序",
+ "Item Count": "關聯物料數量",
+ "Qc Item Count": "關聯品檢項目數量",
+ "Qc Category Count": "關聯品檢模板數量",
+ "Actions": "操作",
+ "View": "查看",
+ "Edit": "編輯",
+ "Delete": "刪除",
+ "Add": "新增",
+ "Add Mapping": "新增映射",
+ "Add Association": "新增關聯",
+ "Save": "儲存",
+ "Cancel": "取消",
+ "Submit": "提交",
+ "Details": "詳情",
+ "Create Qc Category": "新增品檢模板",
+ "Edit Qc Category": "編輯品檢模板",
+ "Create Qc Item": "新增品檢項目",
+ "Edit Qc Item": "編輯品檢項目",
+ "Delete Success": "刪除成功",
+ "Delete Error": "刪除失敗",
+ "Submit Success": "提交成功",
+ "Submit Error": "提交失敗",
+ "Cannot Delete": "無法刪除",
+ "Cannot delete QcCategory. It has {itemCount} item(s) and {qcItemCount} qc item(s) linked to it.": "無法刪除品檢模板。它有 {itemCount} 個物料和 {qcItemCount} 個品檢項目與其關聯。",
+ "Cannot delete QcItem. It is linked to one or more QcCategories.": "無法刪除品檢項目。它與一個或多個品檢模板關聯。",
+ "Select Item": "選擇物料",
+ "Select Qc Category": "選擇品檢模板",
+ "Select Qc Item": "選擇品檢項目",
+ "Select Type": "選擇類型",
+ "Item Code": "物料編號",
+ "Item Name": "物料名稱",
+ "Qc Category Code": "品檢模板編號",
+ "Qc Category Name": "品檢模板名稱",
+ "Qc Item Code": "品檢項目編號",
+ "Qc Item Name": "品檢項目名稱",
+ "Mapping Details": "映射詳情",
+ "Association Details": "關聯詳情",
+ "No mappings found": "未找到映射",
+ "No associations found": "未找到關聯",
+ "No data available": "暫無數據",
+ "Confirm Delete": "確認刪除",
+ "Are you sure you want to delete this item?": "您確定要刪除此項目嗎?"
+}
+