CANCERYS\kw093 1 month ago
parent
commit
c79cf73d1f
27 changed files with 528 additions and 65 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. +2
    -1
      src/app/api/stockIn/index.ts
  5. +6
    -4
      src/components/PoDetail/PoDetail.tsx
  6. +1
    -1
      src/components/PoDetail/PutAwayForm.tsx
  7. +13
    -7
      src/components/PutAwayScan/PutAwayModal.tsx
  8. +28
    -3
      src/components/PutAwayScan/PutAwayScan.tsx
  9. +10
    -8
      src/components/Qc/QcComponent.tsx
  10. +18
    -13
      src/components/Qc/QcStockInModal.tsx
  11. +63
    -0
      src/components/QcCategorySave/QcCategoryDetails.tsx
  12. +121
    -0
      src/components/QcCategorySave/QcCategorySave.tsx
  13. +40
    -0
      src/components/QcCategorySave/QcCategorySaveLoading.tsx
  14. +24
    -0
      src/components/QcCategorySave/QcCategorySaveWrapper.tsx
  15. +1
    -0
      src/components/QcCategorySave/index.ts
  16. +33
    -4
      src/components/QcCategorySearch/QcCategorySearch.tsx
  17. +3
    -3
      src/components/QcItemSave/QcItemDetails.tsx
  18. +1
    -1
      src/components/QcItemSave/QcItemSave.tsx
  19. +8
    -2
      src/components/QcItemSearch/QcItemSearch.tsx
  20. +15
    -1
      src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx
  21. +1
    -1
      src/components/StockIn/FgStockInForm.tsx
  22. +1
    -1
      src/components/StockIn/StockInForm.tsx
  23. +4
    -2
      src/i18n/zh/common.json
  24. +2
    -1
      src/i18n/zh/purchaseOrder.json
  25. +2
    -1
      src/i18n/zh/putAway.json
  26. +13
    -2
      src/i18n/zh/qcCategory.json
  27. +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"] },


+ 2
- 1
src/app/api/stockIn/index.ts View File

@@ -1,5 +1,5 @@
import { cache } from "react";
import "server-only";
import "client-only";
// import { serverFetchJson } from "@/app/utils/fetchUtil";
// import { BASE_API_URL } from "@/config/api";
import { serverFetchJson } from "../../utils/fetchUtil";
@@ -14,6 +14,7 @@ export enum StockInStatus {
RECEIVED = "received",
APPROVED = "escalated",
REJECTED = "rejected",
ESCALATED = "escalated",
COMPLETED = "completed",
PARTIALLY_COMPLETED = "partially_completed",
}


+ 6
- 4
src/components/PoDetail/PoDetail.tsx View File

@@ -63,7 +63,6 @@ import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { WarehouseResult } from "@/app/api/warehouse";
import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
import { CameraContext } from "../Cameras/CameraProvider";
import PoQcStockInModal from "./PoQcStockInModal";
import QrModal from "./QrModal";
import { PlayArrow } from "@mui/icons-material";
import DoneIcon from "@mui/icons-material/Done";
@@ -480,6 +479,8 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
// const [focusField, setFocusField] = useState<HTMLInputElement>();

const purchaseToStockRatio = (row.stockUom.purchaseRatioN ?? 1) / (row.stockUom.purchaseRatioD ?? 1) * (row.stockUom.stockRatioD ?? 1) / (row.stockUom.stockRatioN ?? 1)
const receivedTotal = decimalFormatter.format(row.stockInLine.filter((sil) => sil.purchaseOrderLineId === row.id).reduce((acc, cur) => acc + (cur.acceptedQty ?? 0),0) * purchaseToStockRatio);
const highlightColor = (Number(receivedTotal.replace(/,/g, '')) <= 0) ? "red" : "inherit";
return (
<>
<TableRow
@@ -511,15 +512,16 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
<TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
<TableCell align="left">{row.uom?.udfudesc}</TableCell>
{/* <TableCell align="right">{decimalFormatter.format(row.stockUom.stockQty)}</TableCell> */}
<TableCell align="right">{decimalFormatter.format(row.stockInLine.filter((sil) => sil.purchaseOrderLineId === row.id).reduce((acc, cur) => acc + (cur.acceptedQty ?? 0),0) * purchaseToStockRatio)}</TableCell>
<TableCell align="left">{row.stockUom.stockUomDesc}</TableCell>
<TableCell sx={{ color: highlightColor}} align="right">{receivedTotal}</TableCell>
<TableCell sx={{ color: highlightColor}} align="left">{row.stockUom.stockUomDesc}</TableCell>
{/* <TableCell align="right">
{decimalFormatter.format(totalWeight)} {weightUnit}
</TableCell> */}
{/* <TableCell align="left">{weightUnit}</TableCell> */}
{/* <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell> */}
{/* <TableCell align="left">{row.expiryDate}</TableCell> */}
<TableCell align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell>
<TableCell sx={{ color: highlightColor}} align="left">{t(`${row.status.toLowerCase()}`)}</TableCell>
{/* <TableCell sx={{ color: highlightColor}} align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> */}
{/* <TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell> */}
<TableCell align="center">
<TextField


+ 1
- 1
src/components/PoDetail/PutAwayForm.tsx View File

@@ -116,7 +116,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, setR
return [
{
value: 1,
label: t("W001 - 憶兆 3樓A倉"),
label: t("W201 - 2F-A,B室"),
group: "default",
},
...filteredWarehouse.map((w) => ({


+ 13
- 7
src/components/PutAwayScan/PutAwayModal.tsx View File

@@ -36,6 +36,7 @@ import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { msg } from "../Swal/CustomAlerts";
import { PutAwayRecord } from ".";
import FgStockInForm from "../StockIn/FgStockInForm";
import Swal from "sweetalert2";


interface Props extends Omit<ModalProps, "children"> {
@@ -153,18 +154,22 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
scanner.startScan();
console.log("%c Scanning started ", "color:cyan");
};

useEffect(() => {
if (warehouseId > 0) { // Scanned Warehouse
if (scanner.isScanning) {
setIsOpenScanner(false);
setVerified(true);
msg("貨倉掃瞄成功!")
msg("貨倉掃瞄成功!");
scanner.resetScan();
console.log("%c Scanner reset", "color:cyan");
}
}
}, [warehouseId])

const warehouseDisplay = useMemo(() => {
const wh = warehouse.find((w) => w.id == warehouseId) ?? warehouse.find((w) => w.id == 1);
return <>{wh?.name} <br/> [{wh?.code}]</>;
}, [warehouse, warehouseId, verified]);
// useEffect(() => { // Restart scanner for changing warehouse
// if (warehouseId > 0) {
@@ -379,20 +384,21 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
</Grid>
<Grid container>
<Typography variant="h4" sx={{ fontWeight: 'bold', color: 'black' }} noWrap>
{warehouseId > 0 ? `${warehouse.find((w) => w.id == warehouseId)?.name}`
: `${warehouse.find((w) => w.id == 1)?.name} (建議)`}
{warehouseDisplay} <span style={{fontSize: "45px", color: "black"}}>{verified ? "" : `(建議)`}</span>
</Typography>
</Grid>

</Box>
</Grid>
<Grid item xs={3}>
<Box sx={{ height: '100%', p: 2, textAlign: 'center' }}>
<Box sx={{ height: '100%', p: 2, textAlign: 'center', display: "flex",
flexDirection: "column", justifyContent: "center", }}>
<TextField
type="number" // TODO fix the "e" input
type="number"
label={t("putQty")}
fullWidth
sx={{
flex: 1,
"& .MuiInputBase-input": {
padding: "20px 14px 5px",
fontSize: "50px",
@@ -403,7 +409,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
borderColor: "black",
},
"& .MuiInputLabel-root": {
fontSize: "30px",
fontSize: 30,
top: "-5px",
color: "black",
},


+ 28
- 3
src/components/PutAwayScan/PutAwayScan.tsx View File

@@ -30,7 +30,7 @@ type Props = {
warehouse : WarehouseResult[];
};

type ScanStatusType = "pending" | "rescan";
type ScanStatusType = "pending" | "scanning" | "retry";

const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
const { t } = useTranslation("putAway");
@@ -54,7 +54,7 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
const resetScan = (error : string = "") => {
if (error !== "") {
console.log("%c Scan failed, error: ", "color:red", error);
setScanDisplay("rescan");
setScanDisplay("retry");
} else {
console.log("%c Scan reset", "color:red");
}
@@ -111,6 +111,31 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
}
}, [scanner.result]);

// Get Scanner State
useEffect(() => {
if (scanner.state) {
//
}
}, [scanner.state]);

const displayText = useMemo(() => {
switch (scanner.state) {
case "pending":
return t("Pending scan");
case "scanning":
return t("Scanning");
case "retry":
return t("Rescan");
default:
return t("Pending scan");
}
// if (scanDisplay == "pending") {
// return t("Pending scan");
// } else if (scanDisplay == "retry") {
// return t("Rescan");
// }
}, [scanner.state]);

return (<>
<Paper sx={{
display: 'flex',
@@ -120,7 +145,7 @@ const PutAwayScan: React.FC<Props> = ({ warehouse }) => {
textAlign: 'center',}}
>
<Typography variant="h4">
{scanDisplay == "pending" ? t("Pending scan") : t("Rescan")}
{displayText}
</Typography>
<QrCodeScanner sx={{padding: "10px", fontSize : "150px"}}/>


+ 10
- 8
src/components/Qc/QcComponent.tsx View File

@@ -203,13 +203,13 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {


// Set initial value for acceptQty
useEffect(() => {
if (itemDetail?.demandQty > 0) { //!== undefined) {
setValue("acceptQty", itemDetail.demandQty); // TODO: THIS NEED TO UPDATE TO NOT USE DEMAND QTY
} else {
setValue("acceptQty", itemDetail?.acceptedQty);
}
}, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]);
// useEffect(() => {
// if (itemDetail?.demandQty > 0) { //!== undefined) {
// setValue("acceptQty", itemDetail.demandQty); // TODO: THIS NEED TO UPDATE TO NOT USE DEMAND QTY
// } else {
// setValue("acceptQty", itemDetail?.acceptedQty);
// }
// }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]);

// Fetch Qc Data
useEffect(() => {
@@ -532,6 +532,8 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
if (input) { // Selected Reject in new flow with Error
if (value == "1") { // Selected Accept
input.value = Number(accQty).toString();
} else if (value == "3") {
input.value = "";
} else {
if (Boolean(errors.acceptQty)) {
setValue("acceptQty", 0);
@@ -599,7 +601,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
label={t("rejectQty")}
sx={{ width: '150px' }}
value={
(!Boolean(errors.acceptQty) && qcDecision !== undefined) ?
(!Boolean(errors.acceptQty) && qcDecision !== undefined && qcDecision != 3) ?
(qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty)
: ""
}


+ 18
- 13
src/components/Qc/QcStockInModal.tsx View File

@@ -35,7 +35,7 @@ import { GridRowModesModel } from "@mui/x-data-grid";
import { isEmpty } from "lodash";
import { EscalationCombo } from "@/app/api/user";
import { truncateSync } from "fs";
import { ModalFormInput, StockInLineInput, StockInLine } from "@/app/api/stockIn";
import { ModalFormInput, StockInLineInput, StockInLine, StockInStatus } from "@/app/api/stockIn";
import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions";
import { fetchStockInLineInfo } from "@/app/api/stockIn/actions";
import FgStockInForm from "../StockIn/FgStockInForm";
@@ -83,7 +83,8 @@ const QcStockInModal: React.FC<Props> = ({
} = useTranslation("purchaseOrder");

const [stockInLineInfo, setStockInLineInfo] = useState<StockInLine>();
const [isLoading, setIsLoading] = useState<Boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// const [skipQc, setSkipQc] = useState<Boolean>(false);
// const [viewOnly, setViewOnly] = useState(false);

@@ -120,6 +121,7 @@ const QcStockInModal: React.FC<Props> = ({
// Fetch info if id is input
useEffect(() => {
setIsLoading(true);
setIsSubmitting(false);
if (inputDetail && open) {
console.log("%c Opened Modal with input:", "color:yellow", inputDetail);
if (inputDetail.id) {
@@ -157,7 +159,7 @@ const QcStockInModal: React.FC<Props> = ({
expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined,
receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input")
: dayjs().add(0, "month").format(INPUT_DATE_FORMAT),
acceptQty: d.demandQty?? d.acceptedQty,
acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0,
// escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [],
// qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData],
warehouseId: d.defaultWarehouseId ?? 1,
@@ -195,7 +197,7 @@ const QcStockInModal: React.FC<Props> = ({
const showPutaway = useMemo(() => {
if (stockInLineInfo) {
const status = stockInLineInfo.status;
return status !== "pending" && status !== "escalated" && status !== "rejected";
return status !== StockInStatus.PENDING && status !== StockInStatus.ESCALATED && status !== StockInStatus.REJECTED;
}
return false;
}, [stockInLineInfo]);
@@ -205,10 +207,10 @@ const QcStockInModal: React.FC<Props> = ({
if (stockInLineInfo) {
if (stockInLineInfo.status) {
const status = stockInLineInfo.status;
const isViewOnly = status.toLowerCase() == "completed"
|| status.toLowerCase() == "partially_completed" // TODO update DB
|| status.toLowerCase() == "rejected"
|| (status.toLowerCase() == "escalated" && session?.id != stockInLineInfo.handlerId)
const isViewOnly = status.toLowerCase() == StockInStatus.COMPLETED
|| status.toLowerCase() == StockInStatus.PARTIALLY_COMPLETED // TODO update DB
|| status.toLowerCase() == StockInStatus.REJECTED
|| (status.toLowerCase() == StockInStatus.ESCALATED && session?.id != stockInLineInfo.handlerId)
if (showPutaway) { setTabIndex(1); } else { setTabIndex(0); }
return isViewOnly;
}
@@ -311,7 +313,7 @@ const QcStockInModal: React.FC<Props> = ({
alert("請輸入到期日!");
return;
}
if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && stockInLineInfo?.status != "escalated") { //TODO: fix it please!
if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && stockInLineInfo?.status != StockInStatus.ESCALATED) { //TODO: fix it please!
validationErrors.push("有不合格檢查項目,無法收貨!");
// submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?",
// confirmButtonText: t("confirm putaway"), html: ""});
@@ -321,7 +323,7 @@ const QcStockInModal: React.FC<Props> = ({
// Check if all QC items have results
const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined);
if (itemsWithoutResult.length > 0 && stockInLineInfo?.status != "escalated") { //TODO: fix it please!
if (itemsWithoutResult.length > 0 && stockInLineInfo?.status != StockInStatus.ESCALATED) { //TODO: fix it please!
validationErrors.push(`${t("QC items without result")}`);
// validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`);
}
@@ -368,9 +370,12 @@ const QcStockInModal: React.FC<Props> = ({
handlerId : data.escalationLog?.handlerId,
}
console.log("Escalation Data for submission", escalationLog);

setIsSubmitting(true); //TODO improve
await postStockInLine({...qcData, escalationLog});

} else {
setIsSubmitting(true); //TODO improve
await postStockInLine(qcData);
}

@@ -383,6 +388,7 @@ const QcStockInModal: React.FC<Props> = ({
} else {
closeHandler({}, "backdropClick");
}
setIsSubmitting(false);
msg("已更新來貨狀態");
return ;

@@ -487,8 +493,6 @@ const QcStockInModal: React.FC<Props> = ({
// }, [pafRowSelectionModel, printQty, selectedPrinter]);
}, [stockInLineInfo?.id, pafRowSelectionModel, printQty, selectedPrinter]);
const acceptQty = formProps.watch("acceptedQty")

// const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => {
// const isPassed = qcItems.every((qc) => qc.qcPassed);
// console.log(isPassed)
@@ -585,8 +589,9 @@ const QcStockInModal: React.FC<Props> = ({
color="primary"
sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)}
disabled={isSubmitting || isLoading}
>
{skipQc ? t("confirm") : t("confirm qc result")}
{isSubmitting ? (t("submitting")) : (skipQc ? t("confirm") : t("confirm qc result"))}
</Button>)}
</Stack>
</Box>


+ 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"),


+ 15
- 1
src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx View File

@@ -16,6 +16,8 @@ export interface QrCodeScanner {
stopScan: () => void;
resetScan: () => void;
result: QrCodeInfo | undefined;
state: "scanning" | "pending" | "retry";
error: string | undefined;
}

interface QrCodeScannerProviderProps {
@@ -35,6 +37,8 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
const [leftCurlyBraceCount, setLeftCurlyBraceCount] = useState<number>(0);
const [rightCurlyBraceCount, setRightCurlyBraceCount] = useState<number>(0);
const [scanResult, setScanResult] = useState<QrCodeInfo | undefined>()
const [scanState, setScanState] = useState<"scanning" | "pending" | "retry">("pending");
const [scanError, setScanError] = useState<string | undefined>() // TODO return scan error message

const resetScannerInput = useCallback(() => {
setKeys(() => []);
@@ -51,6 +55,8 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
if (error.length > 0) {
console.log("%c Error:", "color:red", error);
console.log("%c key:", "color:red", keys);
setScanState("retry");
}
}, []);

@@ -127,10 +133,15 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
// Update Qr Code Scanner Values
useEffect(() => {
if (rightCurlyBraceCount > leftCurlyBraceCount || leftCurlyBraceCount > 1) { // Prevent multiple scan
setScanState("retry");
setScanError("Too many scans at once");
resetQrCodeScanner("Too many scans at once");
} else {
if (leftCurlyBraceCount == 1 && keys.length == 1)
{ console.log("%c Scan detected, waiting for inputs...", "color:cyan"); }
{
setScanState("scanning");
console.log("%c Scan detected, waiting for inputs...", "color:cyan");
}
if (
leftCurlyBraceCount !== 0 &&
rightCurlyBraceCount !== 0 &&
@@ -138,6 +149,7 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
) {
const startBrace = keys.indexOf("{");
const endBrace = keys.lastIndexOf("}");
setScanState("pending");
setQrCodeScannerValues((value) => [
...value,
keys.join("").substring(startBrace, endBrace + 1),
@@ -201,6 +213,8 @@ const QrCodeScannerProvider: React.FC<QrCodeScannerProviderProps> = ({
stopScan: endQrCodeScanner,
resetScan: resetQrCodeScanner,
result: scanResult,
state: scanState,
error: scanError,
}}
>
{children}


+ 1
- 1
src/components/StockIn/FgStockInForm.tsx View File

@@ -67,7 +67,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -9px) scale(1)",
transform: "translate(14px, -0.5rem) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 1
- 1
src/components/StockIn/StockInForm.tsx View File

@@ -60,7 +60,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -9px) scale(1)",
transform: "translate(14px, -0.5rem) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 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":"匯入測試",


+ 2
- 1
src/i18n/zh/purchaseOrder.json View File

@@ -165,5 +165,6 @@
"Production Date must be earlier than Expiry Date": "生產日期必須早於到期日",
"confirm expiry date": "確認到期日",
"Invalid Date": "無效日期",
"Missing QC Template, please contact administrator": "找不到品檢模板,請聯絡管理員"
"Missing QC Template, please contact administrator": "找不到品檢模板,請聯絡管理員",
"submitting": "提交中..."
}

+ 2
- 1
src/i18n/zh/putAway.json View File

@@ -20,5 +20,6 @@
"lotNo": "貨品批號",
"poCode": "採購訂單編號",
"itemCode": "貨品編號",
"uom": "單位"
"uom": "單位",
"Scanning": "掃瞄中,請稍後..."
}

+ 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