소스 검색

update qcitem combine page

MergeProblem1
CANCERYS\kw093 2 주 전
부모
커밋
a0febe7794
15개의 변경된 파일1906개의 추가작업 그리고 0개의 파일을 삭제
  1. +19
    -0
      src/app/(main)/settings/qcItem copy/create/not-found.tsx
  2. +26
    -0
      src/app/(main)/settings/qcItem copy/create/page.tsx
  3. +19
    -0
      src/app/(main)/settings/qcItem copy/edit/not-found.tsx
  4. +53
    -0
      src/app/(main)/settings/qcItem copy/edit/page.tsx
  5. +48
    -0
      src/app/(main)/settings/qcItem copy/page.tsx
  6. +47
    -0
      src/app/(main)/settings/qcItemAll/page.tsx
  7. +265
    -0
      src/app/api/settings/qcItemAll/actions.ts
  8. +101
    -0
      src/app/api/settings/qcItemAll/index.ts
  9. +105
    -0
      src/components/QcItemAll/QcItemAllTabs.tsx
  10. +351
    -0
      src/components/QcItemAll/Tab0ItemQcCategoryMapping.tsx
  11. +304
    -0
      src/components/QcItemAll/Tab1QcCategoryQcItemMapping.tsx
  12. +226
    -0
      src/components/QcItemAll/Tab2QcCategoryManagement.tsx
  13. +226
    -0
      src/components/QcItemAll/Tab3QcItemManagement.tsx
  14. +58
    -0
      src/i18n/en/qcItemAll.json
  15. +58
    -0
      src/i18n/zh/qcItemAll.json

+ 19
- 0
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 (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">
{t("The create qc item page was not found!")}
</Typography>
<Link href="/qcItems" component={NextLink} variant="body2">
{t("Return to all qc items")}
</Link>
</Stack>
);
}

+ 26
- 0
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 (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Create Qc Item")}
</Typography>
<I18nProvider namespaces={["qcItem"]}>
<QcItemSave />
</I18nProvider>
</>
);
};

export default qcItem;

+ 19
- 0
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 (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">
{t("The edit qc item page was not found!")}
</Typography>
<Link href="/settings/qcItems" component={NextLink} variant="body2">
{t("Return to all qc items")}
</Link>
</Stack>
);
}

+ 53
- 0
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<Props> = 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 (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Edit Qc Item")}
</Typography>
<I18nProvider namespaces={["qcItem"]}>
<QcItemSave id={id} />
</I18nProvider>
</>
);
};

export default qcItem;

+ 48
- 0
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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Qc Item")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="qcItem/create"
>
{t("Create Qc Item")}
</Button>
</Stack>
<Suspense fallback={<QcItemSearch.Loading />}>
<I18nProvider namespaces={["common", "qcItem"]}>
<QcItemSearch />
</I18nProvider>
</Suspense>
</>
);
};

export default qcItem;

+ 47
- 0
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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
sx={{ mb: 3 }}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Qc Item All")}
</Typography>
</Stack>
<Suspense fallback={<div>Loading...</div>}>
<I18nProvider namespaces={["common", "qcItemAll", "qcCategory", "qcItem"]}>
<QcItemAllTabs
tab0Content={<Tab0ItemQcCategoryMapping />}
tab1Content={<Tab1QcCategoryQcItemMapping />}
tab2Content={<Tab2QcCategoryManagement />}
tab3Content={<Tab3QcItemManagement />}
/>
</I18nProvider>
</Suspense>
</>
);
};

export default qcItemAll;


+ 265
- 0
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<string, string> | 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<string, string> | null;
}

// Item and QcCategory mapping
export const getItemQcCategoryMappings = async (
qcCategoryId?: number,
itemId?: number
): Promise<ItemQcCategoryMappingInfo[]> => {
const params = new URLSearchParams();
if (qcCategoryId) params.append("qcCategoryId", qcCategoryId.toString());
if (itemId) params.append("itemId", itemId.toString());
return serverFetchJson<ItemQcCategoryMappingInfo[]>(
`${BASE_API_URL}/qcItemAll/itemMappings?${params.toString()}`
);
};

export const saveItemQcCategoryMapping = async (
itemId: number,
qcCategoryId: number,
type: string
): Promise<ItemQcCategoryMappingInfo> => {
const params = new URLSearchParams();
params.append("itemId", itemId.toString());
params.append("qcCategoryId", qcCategoryId.toString());
params.append("type", type);
const response = await serverFetchJson<ItemQcCategoryMappingInfo>(
`${BASE_API_URL}/qcItemAll/itemMapping?${params.toString()}`,
{
method: "POST",
}
);
revalidateTag("qcItemAll");
return response;
};

export const deleteItemQcCategoryMapping = async (
mappingId: number
): Promise<void> => {
await serverFetchJson<void>(
`${BASE_API_URL}/qcItemAll/itemMapping/${mappingId}`,
{
method: "DELETE",
}
);
revalidateTag("qcItemAll");
};

// QcCategory and QcItem mapping
export const getQcCategoryQcItemMappings = async (
qcCategoryId: number
): Promise<QcItemInfo[]> => {
return serverFetchJson<QcItemInfo[]>(
`${BASE_API_URL}/qcItemAll/qcItemMappings/${qcCategoryId}`
);
};

export const saveQcCategoryQcItemMapping = async (
qcCategoryId: number,
qcItemId: number,
order: number,
description?: string
): Promise<QcItemInfo> => {
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<QcItemInfo>(
`${BASE_API_URL}/qcItemAll/qcItemMapping?${params.toString()}`,
{
method: "POST",
}
);
revalidateTag("qcItemAll");
return response;
};

export const deleteQcCategoryQcItemMapping = async (
mappingId: number
): Promise<void> => {
await serverFetchJson<void>(
`${BASE_API_URL}/qcItemAll/qcItemMapping/${mappingId}`,
{
method: "DELETE",
}
);
revalidateTag("qcItemAll");
};

// Counts
export const getItemCountByQcCategory = async (
qcCategoryId: number
): Promise<number> => {
return serverFetchJson<number>(
`${BASE_API_URL}/qcItemAll/itemCount/${qcCategoryId}`
);
};

export const getQcItemCountByQcCategory = async (
qcCategoryId: number
): Promise<number> => {
return serverFetchJson<number>(
`${BASE_API_URL}/qcItemAll/qcItemCount/${qcCategoryId}`
);
};

// Validation
export const canDeleteQcCategory = async (id: number): Promise<boolean> => {
return serverFetchJson<boolean>(
`${BASE_API_URL}/qcItemAll/canDeleteQcCategory/${id}`
);
};

export const canDeleteQcItem = async (id: number): Promise<boolean> => {
return serverFetchJson<boolean>(
`${BASE_API_URL}/qcItemAll/canDeleteQcItem/${id}`
);
};

// Save and delete with validation
export const saveQcCategoryWithValidation = async (
data: SaveQcCategoryInputs
): Promise<SaveQcCategoryResponse> => {
const response = await serverFetchJson<SaveQcCategoryResponse>(
`${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<DeleteResponse> => {
const response = await serverFetchJson<DeleteResponse>(
`${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<SaveQcItemResponse> => {
const response = await serverFetchJson<SaveQcItemResponse>(
`${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<DeleteResponse> => {
const response = await serverFetchJson<DeleteResponse>(
`${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<QcCategoryResult[]> => {
return serverFetchJson<QcCategoryResult[]>(`${BASE_API_URL}/qcCategories`, {
next: { tags: ["qcCategories"] },
});
};

export const fetchItemsForAll = async (): Promise<ItemsResult[]> => {
return serverFetchJson<ItemsResult[]>(`${BASE_API_URL}/items`, {
next: { tags: ["items"] },
});
};

export const fetchQcItemsForAll = async (): Promise<QcItemResult[]> => {
return serverFetchJson<QcItemResult[]>(`${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<ItemsResult | null> => {
try {
return await serverFetchJson<ItemsResult>(`${BASE_API_URL}/qcItemAll/itemByCode/${encodeURIComponent(code)}`);
} catch (error) {
// Item not found
return null;
}
};




+ 101
- 0
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;
}


+ 105
- 0
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 (
<div
role="tabpanel"
hidden={value !== index}
id={`qc-item-all-tabpanel-${index}`}
aria-labelledby={`qc-item-all-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
}

interface QcItemAllTabsProps {
tab0Content: ReactNode;
tab1Content: ReactNode;
tab2Content: ReactNode;
tab3Content: ReactNode;
}

const QcItemAllTabs: React.FC<QcItemAllTabsProps> = ({
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 (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={t("Item and Qc Category Mapping")} />
<Tab label={t("Qc Category and Qc Item Mapping")} />
<Tab label={t("Qc Category Management")} />
<Tab label={t("Qc Item Management")} />
</Tabs>
</Box>

<TabPanel value={currentTab} index={0}>
{tab0Content}
</TabPanel>

<TabPanel value={currentTab} index={1}>
{tab1Content}
</TabPanel>

<TabPanel value={currentTab} index={2}>
{tab2Content}
</TabPanel>

<TabPanel value={currentTab} index={3}>
{tab3Content}
</TabPanel>
</Box>
);
};

export default QcItemAllTabs;


+ 351
- 0
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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab0ItemQcCategoryMapping: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null);
const [mappings, setMappings] = useState<ItemQcCategoryMappingInfo[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [itemCode, setItemCode] = useState<string>("");
const [validatedItem, setValidatedItem] = useState<ItemsResult | null>(null);
const [itemCodeError, setItemCodeError] = useState<string>("");
const [validatingItemCode, setValidatingItemCode] = useState<boolean>(false);
const [selectedType, setSelectedType] = useState<string>("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<SearchParamNames>[] = 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<Column<QcCategoryResult>[]>(
() => [
{ 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: <Edit />,
buttonIcons: {} as any,
sx: columnWidthSx("10%"),
},
],
[t, handleViewMappings]
);

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "200px" }}>
<CircularProgress />
</Box>
);
}

return (
<Box>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* View Mappings Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{t("Mapping Details")} - {selectedCategory?.name}
</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddMapping}
>
{t("Add Mapping")}
</Button>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Type")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{mappings.length === 0 ? (
<TableRow>
<TableCell colSpan={4} align="center">
{t("No mappings found")}
</TableCell>
</TableRow>
) : (
mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>{mapping.itemCode}</TableCell>
<TableCell>{mapping.itemName}</TableCell>
<TableCell>{mapping.type}</TableCell>
<TableCell>
<IconButton
color="error"
size="small"
onClick={() => handleDeleteMapping(mapping.id)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>

{/* Add Mapping Dialog */}
<Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add Mapping")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 2 }}>
<TextField
label={t("Item Code")}
value={itemCode}
onChange={(e) => 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 ? <CircularProgress size={20} /> : null,
}}
/>
<TextField
select
label={t("Select Type")}
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
SelectProps={{
native: true,
}}
fullWidth
>
{typeOptions.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</TextField>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleSaveMapping}
disabled={!validatedItem}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

export default Tab0ItemQcCategoryMapping;


+ 304
- 0
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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab1QcCategoryQcItemMapping: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [selectedCategory, setSelectedCategory] = useState<QcCategoryResult | null>(null);
const [mappings, setMappings] = useState<QcItemInfo[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [openAddDialog, setOpenAddDialog] = useState(false);
const [qcItems, setQcItems] = useState<QcItemResult[]>([]);
const [selectedQcItem, setSelectedQcItem] = useState<QcItemResult | null>(null);
const [order, setOrder] = useState<number>(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<SearchParamNames>[] = 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<Column<QcCategoryResult>[]>(
() => [
{ 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: <Edit />,
buttonIcons: {} as any,
sx: columnWidthSx("10%"),
},
],
[t, handleViewMappings]
);

return (
<Box>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* View Mappings Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{t("Association Details")} - {selectedCategory?.name}
</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddMapping}
>
{t("Add Association")}
</Button>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Order")}</TableCell>
<TableCell>{t("Qc Item Code")}</TableCell>
<TableCell>{t("Qc Item Name")}</TableCell>
<TableCell>{t("Description")}</TableCell>
<TableCell>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{mappings.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
{t("No associations found")}
</TableCell>
</TableRow>
) : (
mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>{mapping.order}</TableCell>
<TableCell>{mapping.code}</TableCell>
<TableCell>{mapping.name}</TableCell>
<TableCell>{mapping.description || "-"}</TableCell>
<TableCell>
<IconButton
color="error"
size="small"
onClick={() => handleDeleteMapping(mapping.id)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
</DialogActions>
</Dialog>

{/* Add Mapping Dialog */}
<Dialog open={openAddDialog} onClose={() => setOpenAddDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>{t("Add Association")}</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 2 }}>
<Autocomplete
options={qcItems}
getOptionLabel={(option) => `${option.code} - ${option.name}`}
value={selectedQcItem}
onChange={(_, newValue) => setSelectedQcItem(newValue)}
renderInput={(params) => (
<TextField {...params} label={t("Select Qc Item")} />
)}
/>
<TextField
type="number"
label={t("Order")}
value={order}
onChange={(e) => setOrder(parseInt(e.target.value) || 0)}
fullWidth
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddDialog(false)}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleSaveMapping}
disabled={!selectedQcItem}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};

export default Tab1QcCategoryQcItemMapping;


+ 226
- 0
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<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab2QcCategoryManagement: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcCategories, setQcCategories] = useState<QcCategoryResult[]>([]);
const [filteredQcCategories, setFilteredQcCategories] = useState<QcCategoryResult[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [editingCategory, setEditingCategory] = useState<QcCategoryResult | null>(null);

useEffect(() => {
loadCategories();
}, []);

const loadCategories = async () => {
const categories = await fetchQcCategoriesForAll();
setQcCategories(categories);
setFilteredQcCategories(categories);
};

const formProps = useForm<SaveQcCategoryInputs>({
defaultValues: {
code: "",
name: "",
description: "",
},
});

const searchCriteria: Criterion<SearchParamNames>[] = 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) + "<br>";
}
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<Column<QcCategoryResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: handleEdit,
buttonIcon: <EditNote />,
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: <Delete />,
buttonColor: "error",
sx: columnWidthSx("5%"),
},
],
[t, handleEdit, handleDelete]
);

return (
<>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAdd}
>
{t("Create Qc Category")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcCategories(
qcCategories.filter(
(qc) =>
(!query.code || qc.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qc.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcCategoryResult>
items={filteredQcCategories}
columns={columns}
/>

{/* Add/Edit Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{editingCategory ? t("Edit Qc Category") : t("Create Qc Category")}
</DialogTitle>
<FormProvider {...formProps}>
<form onSubmit={formProps.handleSubmit(handleSubmit)}>
<DialogContent>
<QcCategoryDetails />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
<Button type="submit" variant="contained">
{t("Submit")}
</Button>
</DialogActions>
</form>
</FormProvider>
</Dialog>
</>
);
};

export default Tab2QcCategoryManagement;


+ 226
- 0
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<Omit<QcItemResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const Tab3QcItemManagement: React.FC = () => {
const { t } = useTranslation("qcItemAll");
const [qcItems, setQcItems] = useState<QcItemResult[]>([]);
const [filteredQcItems, setFilteredQcItems] = useState<QcItemResult[]>([]);
const [openDialog, setOpenDialog] = useState(false);
const [editingItem, setEditingItem] = useState<QcItemResult | null>(null);

useEffect(() => {
loadItems();
}, []);

const loadItems = async () => {
const items = await fetchQcItemsForAll();
setQcItems(items);
setFilteredQcItems(items);
};

const formProps = useForm<SaveQcItemInputs>({
defaultValues: {
code: "",
name: "",
description: "",
},
});

const searchCriteria: Criterion<SearchParamNames>[] = 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) + "<br>";
}
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<Column<QcItemResult>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: handleEdit,
buttonIcon: <EditNote />,
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: <Delete />,
buttonColor: "error",
},
],
[t, handleEdit, handleDelete]
);

return (
<>
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAdd}
>
{t("Create Qc Item")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
setFilteredQcItems(
qcItems.filter(
(qi) =>
(!query.code || qi.code.toLowerCase().includes(query.code.toLowerCase())) &&
(!query.name || qi.name.toLowerCase().includes(query.name.toLowerCase()))
)
);
}}
onReset={onReset}
/>
<SearchResults<QcItemResult>
items={filteredQcItems}
columns={columns}
/>

{/* Add/Edit Dialog */}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
{editingItem ? t("Edit Qc Item") : t("Create Qc Item")}
</DialogTitle>
<FormProvider {...formProps}>
<form onSubmit={formProps.handleSubmit(handleSubmit)}>
<DialogContent>
<QcItemDetails />
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>{t("Cancel")}</Button>
<Button type="submit" variant="contained">
{t("Submit")}
</Button>
</DialogActions>
</form>
</FormProvider>
</Dialog>
</>
);
};

export default Tab3QcItemManagement;


+ 58
- 0
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?"
}


+ 58
- 0
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?": "您確定要刪除此項目嗎?"
}


불러오는 중...
취소
저장