Browse Source

update qc master data (WIP)

master
kelvinsuen 1 month ago
parent
commit
4cc3d84fb1
15 changed files with 429 additions and 23 deletions
  1. +53
    -0
      src/app/(main)/settings/qcCategory/edit/page.tsx
  2. +41
    -2
      src/app/api/settings/qcCategory/actions.ts
  3. +12
    -0
      src/app/api/settings/qcCategory/index.ts
  4. +63
    -0
      src/components/QcCategorySave/QcCategoryDetails.tsx
  5. +121
    -0
      src/components/QcCategorySave/QcCategorySave.tsx
  6. +40
    -0
      src/components/QcCategorySave/QcCategorySaveLoading.tsx
  7. +24
    -0
      src/components/QcCategorySave/QcCategorySaveWrapper.tsx
  8. +1
    -0
      src/components/QcCategorySave/index.ts
  9. +33
    -4
      src/components/QcCategorySearch/QcCategorySearch.tsx
  10. +3
    -3
      src/components/QcItemSave/QcItemDetails.tsx
  11. +1
    -1
      src/components/QcItemSave/QcItemSave.tsx
  12. +8
    -2
      src/components/QcItemSearch/QcItemSearch.tsx
  13. +4
    -2
      src/i18n/zh/common.json
  14. +13
    -2
      src/i18n/zh/qcCategory.json
  15. +12
    -7
      src/i18n/zh/qcItem.json

+ 53
- 0
src/app/(main)/settings/qcCategory/edit/page.tsx View File

@@ -0,0 +1,53 @@
import { Metadata } from "next";
import { getServerI18n, I18nProvider } from "@/i18n";
import Typography from "@mui/material/Typography";
import { fetchQcCategoryDetails, preloadQcCategory } from "@/app/api/settings/qcCategory";
import QcCategorySave from "@/components/QcCategorySave";
import { isArray } from "lodash";
import { notFound } from "next/navigation";
import { ServerFetchError } from "@/app/utils/fetchUtil";

export const metadata: Metadata = {
title: "Qc Category",
};

interface Props {
searchParams: { [key: string]: string | string[] | undefined };
}

const qcCategory: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("qcCategory");

const id = searchParams["id"];

if (!id || isArray(id)) {
notFound();
}

try {
console.log("first");
await fetchQcCategoryDetails(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 Category")}
</Typography>
<I18nProvider namespaces={["qcCategory"]}>
<QcCategorySave id={id} />
</I18nProvider>
</>
);
};

export default qcCategory;

+ 41
- 2
src/app/api/settings/qcCategory/actions.ts View File

@@ -2,16 +2,55 @@

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { revalidatePath, revalidateTag } from "next/cache";
import { QcCategoryResult } from ".";

export interface CreateQcCategoryInputs {
code: string;
name: string;
}

export const saveQcCategory = async (data: CreateQcCategoryInputs) => {
return serverFetchJson(`${BASE_API_URL}/qcCategories/save`, {
export const saveQcCategory = async (data: SaveQcCategoryInputs) => {
return serverFetchJson<SaveQcCategoryResponse>(`${BASE_API_URL}/qcCategories/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export interface SaveQcItemsMapping {
id: number;
order: number;
}

export interface SaveQcCategoryInputs {
id?: number;
code: string;
name: string;
description?: string;
qcItems: SaveQcItemsMapping[];
}

export interface SaveQcCategoryResponse {
id?: number;
code: string;
name: string;
description?: string;
errors: Record<keyof SaveQcCategoryInputs, string>;
// qcItems: SaveQcItemsMapping[];
}

export const deleteQcCategory = async (id: number) => {
const response = await serverFetchJson<QcCategoryResult[]>(
`${BASE_API_URL}/qcCategories/${id}`,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
},
);

revalidateTag("qcCategories");
revalidatePath("/(main)/settings/qcCategory");

return response;
};

+ 12
- 0
src/app/api/settings/qcCategory/index.ts View File

@@ -2,11 +2,13 @@ import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";
import { SaveQcCategoryInputs } from "./actions";

export interface QcCategoryResult {
id: number;
code: string;
name: string;
description?: string;
}

export interface QcCategoryCombo {
@@ -25,6 +27,16 @@ export const fetchQcCategories = cache(async () => {
});
});

export const fetchQcCategoryDetails = cache(async (qcCategoryId: string) => {
return serverFetchJson<SaveQcCategoryInputs>(
`${BASE_API_URL}/qcCategories/details/${qcCategoryId}`,
{
next: { tags: [`qcCategoryDetails_${qcCategoryId}`] },
},
);
});


export const fetchQcCategoryCombo = cache(async () => {
return serverFetchJson<QcCategoryCombo[]>(`${BASE_API_URL}/qcCategories/combo`, {
next: { tags: ["qcCategoryCombo"] },


+ 63
- 0
src/components/QcCategorySave/QcCategoryDetails.tsx View File

@@ -0,0 +1,63 @@
import { SaveQcCategoryInputs } from "@/app/api/settings/qcCategory/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

const QcCategoryDetails = () => {
const { t } = useTranslation("qcCategory");
const { register } = useFormContext<SaveQcCategoryInputs>();

return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
{/* <Typography variant={"overline"} display={"block"} marginBlockEnd={1}>
{t("Qc Item Details")}
</Typography> */}
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Code")}
fullWidth
{...register("code", {
required: "Code required!",
maxLength: 30,
})}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Name")}
fullWidth
{...register("name", {
required: "Name required!",
maxLength: 30,
})}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Description")}
// multiline
fullWidth
{...register("description", {
maxLength: 100,
})}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
);
};

export default QcCategoryDetails;

+ 121
- 0
src/components/QcCategorySave/QcCategorySave.tsx View File

@@ -0,0 +1,121 @@
"use client";

import {
deleteQcCategory,
saveQcCategory,
SaveQcCategoryInputs,
} from "@/app/api/settings/qcCategory/actions";
import { Button, Stack } from "@mui/material";
import { useCallback } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import {
deleteDialog,
errorDialogWithContent,
submitDialog,
successDialog,
} from "../Swal/CustomAlerts";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import QcCategoryDetails from "./QcCategoryDetails";
import { Check, Close, Delete } from "@mui/icons-material";

interface Props {
defaultInputs?: SaveQcCategoryInputs;
}

const QcCategorySave: React.FC<Props> = ({ defaultInputs }) => {
const { t } = useTranslation("qcCategory");
const router = useRouter();

const formProps = useForm<SaveQcCategoryInputs>({
defaultValues: {
...defaultInputs,
},
});

const handleSubmit = useCallback(async (data: SaveQcCategoryInputs) => {
const response = await saveQcCategory(data);

const errors = response.errors;
if (errors) {
let errorContents = "";
for (const [key, value] of Object.entries(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, () =>
router.push("/settings/qcCategory"),
);
}
}, []);

const onSubmit = useCallback<SubmitHandler<SaveQcCategoryInputs>>(
async (data) => {
await submitDialog(() => handleSubmit(data), t);
},
[],
);

const handleCancel = () => {
router.replace("/settings/qcCategory");
};

const handleDelete = () => {
deleteDialog(async () => {
await deleteQcCategory(formProps.getValues("id")!);

await successDialog(t("Delete Success"), t, () =>
router.replace("/settings/qcCategory"),
);
}, t);
};

return (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component={"form"}
onSubmit={formProps.handleSubmit(onSubmit)}
>
<QcCategoryDetails />
<Stack direction="row" justifyContent="flex-end" gap={1}>
{defaultInputs?.id && (
<Button
variant="outlined"
color="error"
startIcon={<Delete />}
onClick={handleDelete}
>
{t("Delete")}
</Button>
)}
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
>
{t("Submit")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};

export default QcCategorySave;

+ 40
- 0
src/components/QcCategorySave/QcCategorySaveLoading.tsx View File

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const QcItemSaveLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default QcItemSaveLoading;

+ 24
- 0
src/components/QcCategorySave/QcCategorySaveWrapper.tsx View File

@@ -0,0 +1,24 @@
import React from "react";
import QcCategorySaveLoading from "./QcCategorySaveLoading";
import QcCategorySave from "./QcCategorySave";
import { fetchQcCategoryDetails } from "@/app/api/settings/qcCategory";

interface SubComponents {
Loading: typeof QcCategorySaveLoading;
}

type SaveQcCategoryProps = {
id?: string;
};

type Props = SaveQcCategoryProps;

const QcCategorySaveWrapper: React.FC<Props> & SubComponents = async (props) => {
const qcCategory = props.id ? await fetchQcCategoryDetails(props.id) : undefined;

return <QcCategorySave defaultInputs={qcCategory} />;
};

QcCategorySaveWrapper.Loading = QcCategorySaveLoading;

export default QcCategorySaveWrapper;

+ 1
- 0
src/components/QcCategorySave/index.ts View File

@@ -0,0 +1 @@
export { default } from "./QcCategorySaveWrapper";

+ 33
- 4
src/components/QcCategorySearch/QcCategorySearch.tsx View File

@@ -6,6 +6,10 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import { QcCategoryResult } from "@/app/api/settings/qcCategory";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { deleteQcCategory } from "@/app/api/settings/qcCategory/actions";
import Delete from "@mui/icons-material/Delete";
import { usePathname, useRouter } from "next/navigation";

interface Props {
qcCategories: QcCategoryResult[];
@@ -15,7 +19,9 @@ type SearchQuery = Partial<Omit<QcCategoryResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const QcCategorySearch: React.FC<Props> = ({ qcCategories }) => {
const { t } = useTranslation("qcCategories");
const { t } = useTranslation("qcCategory");
const router = useRouter();
const pathname = usePathname();

// If qcCategory searching is done on the server-side, then no need for this.
const [filteredQcCategories, setFilteredQcCategories] =
@@ -34,8 +40,21 @@ const QcCategorySearch: React.FC<Props> = ({ qcCategories }) => {
}, [qcCategories]);

const onQcCategoryClick = useCallback((qcCategory: QcCategoryResult) => {
console.log(qcCategory);
router.push(`${pathname}/edit?id=${qcCategory.id}`);
}, [router]);

const handleDelete = useCallback((qcCategory: QcCategoryResult) => {
deleteDialog(async () => {
qcCategories = await deleteQcCategory(qcCategory.id);
setFilteredQcCategories(qcCategories);

await successDialog(t("Delete Success"), t);
}, t);
}, []);
const columnWidthSx = (width = "10%") => {
return { width: width, whiteSpace: "nowrap" };
};

const columns = useMemo<Column<QcCategoryResult>[]>(
() => [
@@ -44,9 +63,19 @@ const QcCategorySearch: React.FC<Props> = ({ qcCategories }) => {
label: t("Details"),
onClick: onQcCategoryClick,
buttonIcon: <EditNote />,
sx: columnWidthSx("5%"),
},
{ name: "code", label: t("Code"), sx: columnWidthSx("15%"), },
{ name: "name", label: t("Name"), sx: columnWidthSx("30%"), },
// { name: "description", label: t("Description"), sx: columnWidthSx("50%"), },
{
name: "id",
label: t("Delete"),
onClick: handleDelete,
buttonIcon: <Delete />,
buttonColor: "error",
sx: columnWidthSx("5%"),
},
{ name: "code", label: t("Code") },
{ name: "name", label: t("Name") },
],
[t, onQcCategoryClick],
);


+ 3
- 3
src/components/QcItemSave/QcItemDetails.tsx View File

@@ -12,16 +12,16 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

const QcItemDetails = () => {
const { t } = useTranslation();
const { t } = useTranslation("qcItem");
const { register } = useFormContext<SaveQcItemInputs>();

return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant={"overline"} display={"block"} marginBlockEnd={1}>
{/* <Typography variant={"overline"} display={"block"} marginBlockEnd={1}>
{t("Qc Item Details")}
</Typography>
</Typography> */}
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField


+ 1
- 1
src/components/QcItemSave/QcItemSave.tsx View File

@@ -63,7 +63,7 @@ const QcItemSave: React.FC<Props> = ({ defaultInputs }) => {
);

const handleCancel = () => {
router.replace("/qcItem");
router.replace("/settings/qcItem");
};

const handleDelete = () => {


+ 8
- 2
src/components/QcItemSearch/QcItemSearch.tsx View File

@@ -61,16 +61,22 @@ const QcItemSearch: React.FC<Props> = ({ qcItems }) => {
}, t);
}, []);

const columnWidthSx = (width = "10%") => {
return { width: width, whiteSpace: "nowrap" };
};

const columns = useMemo<Column<QcItemResult>[]>(
() => [
{
name: "id",
label: t("Details"),
sx: columnWidthSx("150px"),
onClick: onQcItemClick,
buttonIcon: <EditNote />,
},
{ name: "code", label: t("Code") },
{ name: "name", label: t("Name") },
{ name: "code", label: t("Code"), sx: columnWidthSx() },
{ name: "name", label: t("Name"), sx: columnWidthSx() },
{ name: "description", label: t("Description") },
{
name: "id",
label: t("Delete"),


+ 4
- 2
src/i18n/zh/common.json View File

@@ -50,8 +50,10 @@
"Delivery Order":"送貨訂單",
"Detail Scheduling":"詳細排程",
"Customer":"客戶",
"QC Check Item":"QC檢查項目",
"QC Category":"QC分類",
"qcItem":"品檢項目",
"QC Check Item":"QC品檢項目",
"QC Category":"QC品檢模板",
"qcCategory":"品檢模板",
"QC Check Template":"QC檢查模板",
"Mail":"郵件",
"Import Testing":"匯入測試",


+ 13
- 2
src/i18n/zh/qcCategory.json View File

@@ -1,9 +1,20 @@
{
"Qc Category": "QC 類別",
"Qc Category": "品檢模板",
"Qc Category List": "QC 類別列表",
"Qc Category Name": "QC 類別名稱",
"Qc Category Description": "QC 類別描述",
"Qc Category Status": "QC 類別狀態",
"Qc Category Created At": "QC 類別創建時間",
"Qc Category Updated At": "QC 類別更新時間"
"Qc Category Updated At": "QC 類別更新時間",
"Name": "名稱",
"Code": "編號",
"Description": "描述",
"Details": "詳情",
"Delete": "刪除",
"Qc Item": "QC 項目",
"Create Qc Category": "新增品檢模板",
"Edit Qc Item": "編輯品檢項目",
"Qc Item Details": "品檢項目詳情",
"Cancel": "取消",
"Submit": "儲存"
}

+ 12
- 7
src/i18n/zh/qcItem.json View File

@@ -1,8 +1,13 @@
{
"Name": "名稱",
"Code": "代碼",
"Details": "詳細資料",
"Delete": "刪除",
"Qc Item": "QC 項目",
"Create Qc Item": "新增 QC 項目"
}
"Name": "名稱",
"Code": "編號",
"Description": "描述",
"Details": "詳情",
"Delete": "刪除",
"Qc Item": "QC 項目",
"Create Qc Item": "新增 QC 項目",
"Edit Qc Item": "編輯品檢項目",
"Qc Item Details": "品檢項目詳情",
"Cancel": "取消",
"Submit": "儲存"
}

Loading…
Cancel
Save