diff --git a/src/app/(main)/settings/qcItem copy/create/not-found.tsx b/src/app/(main)/settings/qcItem copy/create/not-found.tsx new file mode 100644 index 0000000..b36d891 --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/create/not-found.tsx @@ -0,0 +1,19 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("qcItem", "common"); + + return ( + + {t("Not Found")} + + {t("The create qc item page was not found!")} + + + {t("Return to all qc items")} + + + ); +} diff --git a/src/app/(main)/settings/qcItem copy/create/page.tsx b/src/app/(main)/settings/qcItem copy/create/page.tsx new file mode 100644 index 0000000..1cb5c8a --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/create/page.tsx @@ -0,0 +1,26 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSave from "@/components/QcItemSave"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +const qcItem: React.FC = async () => { + const { t } = await getServerI18n("qcItem"); + + return ( + <> + + {t("Create Qc Item")} + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItem copy/edit/not-found.tsx b/src/app/(main)/settings/qcItem copy/edit/not-found.tsx new file mode 100644 index 0000000..e9e09bc --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/edit/not-found.tsx @@ -0,0 +1,19 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("qcItem", "common"); + + return ( + + {t("Not Found")} + + {t("The edit qc item page was not found!")} + + + {t("Return to all qc items")} + + + ); +} diff --git a/src/app/(main)/settings/qcItem copy/edit/page.tsx b/src/app/(main)/settings/qcItem copy/edit/page.tsx new file mode 100644 index 0000000..0e433fb --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/edit/page.tsx @@ -0,0 +1,53 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { fetchQcItemDetails, preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSave from "@/components/QcItemSave"; +import { isArray } from "lodash"; +import { notFound } from "next/navigation"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +interface Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +const qcItem: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("qcItem"); + + const id = searchParams["id"]; + + if (!id || isArray(id)) { + notFound(); + } + + try { + console.log("first"); + await fetchQcItemDetails(id); + console.log("firsts"); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log(e); + notFound(); + } + } + + return ( + <> + + {t("Edit Qc Item")} + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItem copy/page.tsx b/src/app/(main)/settings/qcItem copy/page.tsx new file mode 100644 index 0000000..f1b4e71 --- /dev/null +++ b/src/app/(main)/settings/qcItem copy/page.tsx @@ -0,0 +1,48 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Button, Link, Stack } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { Suspense } from "react"; +import { preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSearch from "@/components/QcItemSearch"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +const qcItem: React.FC = async () => { + const { t } = await getServerI18n("qcItem"); + + preloadQcItem(); + + return ( + <> + + + {t("Qc Item")} + + + + }> + + + + + + ); +}; + +export default qcItem; diff --git a/src/app/(main)/settings/qcItemAll/page.tsx b/src/app/(main)/settings/qcItemAll/page.tsx new file mode 100644 index 0000000..ff0d328 --- /dev/null +++ b/src/app/(main)/settings/qcItemAll/page.tsx @@ -0,0 +1,47 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Stack } from "@mui/material"; +import { Suspense } from "react"; +import QcItemAllTabs from "@/components/QcItemAll/QcItemAllTabs"; +import Tab0ItemQcCategoryMapping from "@/components/QcItemAll/Tab0ItemQcCategoryMapping"; +import Tab1QcCategoryQcItemMapping from "@/components/QcItemAll/Tab1QcCategoryQcItemMapping"; +import Tab2QcCategoryManagement from "@/components/QcItemAll/Tab2QcCategoryManagement"; +import Tab3QcItemManagement from "@/components/QcItemAll/Tab3QcItemManagement"; + +export const metadata: Metadata = { + title: "Qc Item All", +}; + +const qcItemAll: React.FC = async () => { + const { t } = await getServerI18n("qcItemAll"); + + return ( + <> + + + {t("Qc Item All")} + + + Loading...}> + + } + tab1Content={} + tab2Content={} + tab3Content={} + /> + + + + ); +}; + +export default qcItemAll; + diff --git a/src/app/api/settings/qcItemAll/actions.ts b/src/app/api/settings/qcItemAll/actions.ts new file mode 100644 index 0000000..e3a8dde --- /dev/null +++ b/src/app/api/settings/qcItemAll/actions.ts @@ -0,0 +1,265 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { + ItemQcCategoryMappingInfo, + QcItemInfo, + DeleteResponse, + QcCategoryResult, + ItemsResult, + QcItemResult, +} from "."; + +export interface SaveQcCategoryInputs { + id?: number; + code: string; + name: string; + description?: string; +} + +export interface SaveQcCategoryResponse { + id?: number; + code: string; + name: string; + description?: string; + errors: Record | null; +} + +export interface SaveQcItemInputs { + id?: number; + code: string; + name: string; + description?: string; +} + +export interface SaveQcItemResponse { + id?: number; + code: string; + name: string; + description?: string; + errors: Record | null; +} + +// Item and QcCategory mapping +export const getItemQcCategoryMappings = async ( + qcCategoryId?: number, + itemId?: number +): Promise => { + const params = new URLSearchParams(); + if (qcCategoryId) params.append("qcCategoryId", qcCategoryId.toString()); + if (itemId) params.append("itemId", itemId.toString()); + + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMappings?${params.toString()}` + ); +}; + +export const saveItemQcCategoryMapping = async ( + itemId: number, + qcCategoryId: number, + type: string +): Promise => { + const params = new URLSearchParams(); + params.append("itemId", itemId.toString()); + params.append("qcCategoryId", qcCategoryId.toString()); + params.append("type", type); + + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMapping?${params.toString()}`, + { + method: "POST", + } + ); + + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteItemQcCategoryMapping = async ( + mappingId: number +): Promise => { + await serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemMapping/${mappingId}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItemAll"); +}; + +// QcCategory and QcItem mapping +export const getQcCategoryQcItemMappings = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMappings/${qcCategoryId}` + ); +}; + +export const saveQcCategoryQcItemMapping = async ( + qcCategoryId: number, + qcItemId: number, + order: number, + description?: string +): Promise => { + const params = new URLSearchParams(); + params.append("qcCategoryId", qcCategoryId.toString()); + params.append("qcItemId", qcItemId.toString()); + params.append("order", order.toString()); + if (description) params.append("description", description); + + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMapping?${params.toString()}`, + { + method: "POST", + } + ); + + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcCategoryQcItemMapping = async ( + mappingId: number +): Promise => { + await serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemMapping/${mappingId}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItemAll"); +}; + +// Counts +export const getItemCountByQcCategory = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/itemCount/${qcCategoryId}` + ); +}; + +export const getQcItemCountByQcCategory = async ( + qcCategoryId: number +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/qcItemCount/${qcCategoryId}` + ); +}; + +// Validation +export const canDeleteQcCategory = async (id: number): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/canDeleteQcCategory/${id}` + ); +}; + +export const canDeleteQcItem = async (id: number): Promise => { + return serverFetchJson( + `${BASE_API_URL}/qcItemAll/canDeleteQcItem/${id}` + ); +}; + +// Save and delete with validation +export const saveQcCategoryWithValidation = async ( + data: SaveQcCategoryInputs +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/saveQcCategory`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + revalidateTag("qcCategories"); + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcCategoryWithValidation = async ( + id: number +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/deleteQcCategory/${id}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcCategories"); + revalidateTag("qcItemAll"); + revalidatePath("/(main)/settings/qcItemAll"); + return response; +}; + +export const saveQcItemWithValidation = async ( + data: SaveQcItemInputs +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/saveQcItem`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + revalidateTag("qcItems"); + revalidateTag("qcItemAll"); + return response; +}; + +export const deleteQcItemWithValidation = async ( + id: number +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/qcItemAll/deleteQcItem/${id}`, + { + method: "DELETE", + } + ); + + revalidateTag("qcItems"); + revalidateTag("qcItemAll"); + revalidatePath("/(main)/settings/qcItemAll"); + return response; +}; + +// Server actions for fetching data (to be used in client components) +export const fetchQcCategoriesForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/qcCategories`, { + next: { tags: ["qcCategories"] }, + }); +}; + +export const fetchItemsForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/items`, { + next: { tags: ["items"] }, + }); +}; + +export const fetchQcItemsForAll = async (): Promise => { + return serverFetchJson(`${BASE_API_URL}/qcItems`, { + next: { tags: ["qcItems"] }, + }); +}; + +// Get item by code (for Tab 0 - validate item code input) +export const getItemByCode = async (code: string): Promise => { + try { + return await serverFetchJson(`${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 ( + + ); +} + +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 */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {t("Mapping Details")} - {selectedCategory?.name} + + + + + + + + + + + {t("Item Code")} + {t("Item Name")} + {t("Type")} + {t("Actions")} + + + + {mappings.length === 0 ? ( + + + {t("No mappings found")} + + + ) : ( + mappings.map((mapping) => ( + + {mapping.itemCode} + {mapping.itemName} + {mapping.type} + + handleDeleteMapping(mapping.id)} + > + + + + + )) + )} + +
+
+
+
+ + + +
+ + {/* Add Mapping Dialog */} + setOpenAddDialog(false)} maxWidth="sm" fullWidth> + {t("Add Mapping")} + + + handleItemCodeChange(e.target.value)} + error={!!itemCodeError} + helperText={itemCodeError || (validatedItem ? `${validatedItem.code} - ${validatedItem.name}` : t("Enter item code to validate"))} + fullWidth + disabled={validatingItemCode} + InputProps={{ + endAdornment: validatingItemCode ? : null, + }} + /> + setSelectedType(e.target.value)} + SelectProps={{ + native: true, + }} + fullWidth + > + {typeOptions.map((type) => ( + + ))} + + + + + + + + +
+ ); +}; + +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 */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {t("Association Details")} - {selectedCategory?.name} + + + + + + + + + + + {t("Order")} + {t("Qc Item Code")} + {t("Qc Item Name")} + {t("Description")} + {t("Actions")} + + + + {mappings.length === 0 ? ( + + + {t("No associations found")} + + + ) : ( + mappings.map((mapping) => ( + + {mapping.order} + {mapping.code} + {mapping.name} + {mapping.description || "-"} + + handleDeleteMapping(mapping.id)} + > + + + + + )) + )} + +
+
+
+
+ + + +
+ + {/* Add Mapping Dialog */} + setOpenAddDialog(false)} maxWidth="sm" fullWidth> + {t("Add Association")} + + + `${option.code} - ${option.name}`} + value={selectedQcItem} + onChange={(_, newValue) => setSelectedQcItem(newValue)} + renderInput={(params) => ( + + )} + /> + setOrder(parseInt(e.target.value) || 0)} + fullWidth + /> + + + + + + + +
+ ); +}; + +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 ( + <> + + + + { + 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 */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {editingCategory ? t("Edit Qc Category") : t("Create Qc Category")} + + +
+ + + + + + + +
+
+
+ + ); +}; + +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 ( + <> + + + + { + 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 */} + setOpenDialog(false)} maxWidth="md" fullWidth> + + {editingItem ? t("Edit Qc Item") : t("Create Qc Item")} + + +
+ + + + + + + +
+
+
+ + ); +}; + +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?": "您確定要刪除此項目嗎?" +} +