CANCERYS\kw093 2 ay önce
ebeveyn
işleme
8daefeb0de
16 değiştirilmiş dosya ile 749 ekleme ve 50 silme
  1. +1
    -1
      src/app/(main)/jo/page.tsx
  2. +20
    -3
      src/app/api/jo/actions.ts
  3. +24
    -1
      src/app/api/jo/index.ts
  4. +14
    -0
      src/app/api/settings/item/index.ts
  5. +1
    -0
      src/app/api/stockIn/actions.ts
  6. +1
    -0
      src/app/api/stockIn/index.ts
  7. +5
    -1
      src/components/DashboardPage/escalation/EscalationLogTable.tsx
  8. +2
    -0
      src/components/JoSearch/JoCreateFormModal.tsx
  9. +147
    -24
      src/components/JoSearch/JoSearch.tsx
  10. +2
    -2
      src/components/PoDetail/PutAwayForm.tsx
  11. +8
    -3
      src/components/PoDetail/QcComponent.tsx
  12. +54
    -9
      src/components/PoDetail/QcStockInModal.tsx
  13. +6
    -1
      src/components/PutAwayScan/PutAwayModal.tsx
  14. +442
    -0
      src/components/StockIn/FgStockInForm.tsx
  15. +16
    -4
      src/i18n/zh/jo.json
  16. +6
    -1
      src/i18n/zh/purchaseOrder.json

+ 1
- 1
src/app/(main)/jo/page.tsx Dosyayı Görüntüle

@@ -26,7 +26,7 @@ const jo: React.FC = async () => {
{t("Job Order")}
</Typography>
</Stack>
<I18nProvider namespaces={["jo", "common"]}>
<I18nProvider namespaces={["jo", "common", "purchaseOrder", "dashboard"]}> {/* TODO: Improve */}
<Suspense fallback={<JoSearch.Loading />}>
<JoSearch />
</Suspense>


+ 20
- 3
src/app/api/jo/actions.ts Dosyayı Görüntüle

@@ -2,7 +2,7 @@

import { cache } from 'react';
import { Pageable, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { JoStatus, Machine, Operator } from ".";
import { JobOrder, JoStatus, Machine, Operator } from ".";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil";
@@ -21,14 +21,15 @@ export interface SaveJoResponse {

export interface SearchJoResultRequest extends Pageable {
code: string;
name: string;
itemName?: string;
}

export interface SearchJoResultResponse {
records: SearchJoResult[];
records: JobOrder[];
total: number;
}

// DEPRECIATED
export interface SearchJoResult {
id: number;
code: string;
@@ -39,6 +40,11 @@ export interface SearchJoResult {
status: JoStatus;
}

export interface UpdateJoRequest {
id: number;
status: string;
}

// For Jo Button Actions
export interface CommonActionJoRequest {
id: number;
@@ -79,6 +85,7 @@ export interface JobOrderDetail {
pickLines: any[];
status: string;
}

export interface UnassignedJobOrderPickOrder {
pickOrderId: number;
pickOrderCode: string;
@@ -223,6 +230,7 @@ export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: numbe
},
);
});

export const fetchJobOrderDetailByCode = cache(async (code: string) => {
return serverFetchJson<JobOrderDetail>(
`${BASE_API_URL}/jo/detailByCode/${code}`,
@@ -275,6 +283,15 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => {
return response
})

export const updateJo = cache(async (data: UpdateJoRequest) => {
return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/update`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
})
})

export const releaseJo = cache(async (data: CommonActionJoRequest) => {
const response = serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/release`,
{


+ 24
- 1
src/app/api/jo/index.ts Dosyayı Görüntüle

@@ -3,6 +3,8 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import { Item } from "../settings/item";
import { Uom } from "../settings/uom";

export type JoStatus = "planning" | "pending" | "processing" | "packaging" | "storing" | "completed"
export type JoBomMaterialStatus = "pending" | "completed"
@@ -13,6 +15,24 @@ export interface Operator {
username: string;
}

export interface JobOrder {
id: number;
code: string;
reqQty: number;
item: Item;
itemName: string;
// uom: Uom;
pickLines?: JoDetailPickLine[];
status: JoStatus;
planStart?: number[];
planEnd?: number[];
type: string;
// TODO pack below into StockInLineInfo
stockInLineId?: number;
stockInLineStatus?: string;
silHandlerId?: number;
}

export interface Machine {
id: number;
name: string;
@@ -24,14 +44,17 @@ export interface JoDetail {
id: number;
code: string;
itemCode: string;
itemName?: string;
name: string;
reqQty: number;
// itemId: number;
uom: string;
pickLines: JoDetailPickLine[];
status: JoStatus;
planStart: number[];
planEnd: number[];
type: string;
// item?: Item;
}

export interface JoDetailPickLine {
@@ -52,7 +75,7 @@ export interface JoDetailPickedLotNo {
}

export const fetchJoDetail = cache(async (id: number) => {
return serverFetchJson<JoDetail>(`${BASE_API_URL}/jo/detail/${id}`,
return serverFetchJson<JobOrder>(`${BASE_API_URL}/jo/detail/${id}`,
{
method: "GET",
headers: { "Content-Type": "application/json"},


+ 14
- 0
src/app/api/settings/item/index.ts Dosyayı Görüntüle

@@ -5,6 +5,7 @@ import "server-only";
import { serverFetchJson } from "../../../utils/fetchUtil";
import { BASE_API_URL } from "../../../../config/api";
import { QcCategoryResult } from "../qcCategory";
import { Uom } from "../uom";

// import { TypeInputs, UomInputs, WeightUnitInputs } from "./actions";

@@ -24,6 +25,19 @@ export type ItemsResultResponse = {
total: number;
};

export type Item = {
id: number;
code: string;
name: string;
description?: string;
remarks?: string;
type?: string;
shelfLife?: number;
countryOfOrigin?: string;
qcCategory?: QcCategoryResult;
uom: Uom;
}

export type ItemsResult = {
id: string | number;
code: string;


+ 1
- 0
src/app/api/stockIn/actions.ts Dosyayı Görüntüle

@@ -30,6 +30,7 @@ export interface StockInLineEntry {
acceptedQty: number;
purchaseOrderId?: number;
purchaseOrderLineId?: number;
jobOrderId?: number;
status?: string;
expiryDate?: string;
productLotNo?: string;


+ 1
- 0
src/app/api/stockIn/index.ts Dosyayı Görüntüle

@@ -101,6 +101,7 @@ export interface StockInLine {
stockInId?: number;
purchaseOrderId?: number;
purchaseOrderLineId: number;
jobOrderId: number;
itemId: number;
itemNo: string;
itemName: string;


+ 5
- 1
src/components/DashboardPage/escalation/EscalationLogTable.tsx Dosyayı Görüntüle

@@ -40,7 +40,11 @@ const EscalationLogTable: React.FC<Props> = ({
const [escalationLogs, setEscalationLogs] = useState<EscalationResult[]>([]);
const useCardFilter = useContext(CardFilterContext);
const showCompleted = useCardFilter.filter;
const showCompleted = useMemo(() => {
if (type === "dashboard") {
return useCardFilter.filter;
} else { return true}
}, [useCardFilter.filter]);
useEffect(() => {
if (showCompleted) {


+ 2
- 0
src/components/JoSearch/JoCreateFormModal.tsx Dosyayı Görüntüle

@@ -11,6 +11,7 @@ import { isFinite } from "lodash";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react";
import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { msg } from "../Swal/CustomAlerts";

interface Props {
open: boolean;
@@ -54,6 +55,7 @@ const JoCreateFormModal: React.FC<Props> = ({
const response = await manualCreateJo(data)
if (response) {
onSearch();
msg(t("update success"));
onModalClose();
}
}, [])


+ 147
- 24
src/components/JoSearch/JoSearch.tsx Dosyayı Görüntüle

@@ -1,34 +1,41 @@
"use client"
import { SearchJoResult, SearchJoResultRequest, fetchJos } from "@/app/api/jo/actions";
import { SearchJoResultRequest, fetchJos, updateJo } from "@/app/api/jo/actions";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Criterion } from "../SearchBox";
import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults";
import { EditNote } from "@mui/icons-material";
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { arrayToDateString, integerFormatter } from "@/app/utils/formatUtil";
import { orderBy, uniqBy, upperFirst } from "lodash";
import SearchBox from "../SearchBox/SearchBox";
import { useRouter } from "next/navigation";
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { JoDetail } from "@/app/api/jo";
import { StockInLineInput } from "@/app/api/stockIn";
import { JobOrder, JoStatus } from "@/app/api/jo";
import { Button, Stack } from "@mui/material";
import { BomCombo } from "@/app/api/bom";
import JoCreateFormModal from "./JoCreateFormModal";
import AddIcon from '@mui/icons-material/Add';
import QcStockInModal from "../PoDetail/QcStockInModal";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { createStockInLine } from "@/app/api/stockIn/actions";
import { msg } from "../Swal/CustomAlerts";
import dayjs from "dayjs";

interface Props {
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
}

type SearchQuery = Partial<Omit<SearchJoResult, "id">>;
type SearchQuery = Partial<Omit<JobOrder, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
const { t } = useTranslation("jo");
const router = useRouter()
const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]);
const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
const [inputs, setInputs] = useState(defaultInputs);
const [pagingController, setPagingController] = useState(
defaultPagingController
@@ -38,28 +45,28 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Item Name"), paramName: "name", type: "text" },
{ label: t("Item Name"), paramName: "itemName", type: "text" },
], [t])

const columns = useMemo<Column<SearchJoResult>[]>(
const columns = useMemo<Column<JobOrder>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: (record) => onDetailClick(record),
buttonIcon: <EditNote />,
},
{
name: "code",
label: t("Code")
},
{
name: "itemCode",
label: t("Item Code")
name: "item",
label: t("Item Code"),
renderCell: (row) => {
return t(row.item.code)
}
},
{
name: "name",
name: "itemName",
label: t("Item Name"),
renderCell: (row) => {
return t(row.item.name)
}
},
{
name: "reqQty",
@@ -71,12 +78,12 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
}
},
{
name: "uom",
name: "item",
label: t("UoM"),
align: "left",
headerAlign: "left",
renderCell: (row) => {
return t(row.uom)
return t(row.item.uom.udfudesc)
}
},
{
@@ -85,17 +92,69 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
renderCell: (row) => {
return t(upperFirst(row.status))
}
}
},
{
// TODO put it inside Action Buttons
name: "id",
label: t("Actions"),
// onClick: (record) => onDetailClick(record),
// buttonIcon: <EditNote />,
renderCell: (row) => {
const btnSx = getButtonSx(row);
return (
<Button
id="emailSupplier"
type="button"
variant="contained"
color="primary"
sx={{ width: "150px", backgroundColor: btnSx.color }}
// disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
onClick={() => onDetailClick(row)}
>{btnSx.label}</Button>
)
}
},
], []
)

const handleUpdate = useCallback(async (jo: JobOrder) => {
console.log(jo);
try {
// setIsUploading(true)
if (jo.id) {
const response = await updateJo({ id: jo.id, status: "storing" });
console.log(`%c Updated JO:`, "color:lime", response);
const postData = {
itemId: jo?.item?.id!!,
acceptedQty: jo?.reqQty ?? 1,
productLotNo: jo?.code,
productionDate: arrayToDateString(dayjs(), "input"),
jobOrderId: jo?.id,
// acceptedQty: secondReceiveQty || 0,
// acceptedQty: row.acceptedQty,
};
const res = await createStockInLine(postData);
console.log(`%c Created Stock In Line`, "color:lime", res);
msg(t("update success"));
refetchData(defaultInputs, "search");
}

} catch (e) {
// backend error
// setServerError(t("An error has occurred. Please try again later."));
console.log(e);
} finally {
// setIsUploading(false)
}
}, [])

const refetchData = useCallback(async (
query: Record<SearchParamNames, string> | SearchJoResultRequest,
actionType: "reset" | "search" | "paging",
) => {
const params: SearchJoResultRequest = {
code: query.code,
name: query.name,
itemName: query.itemName,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
}
@@ -124,14 +183,69 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
searchDataByPage();
}, [pagingController]);

const onDetailClick = useCallback((record: SearchJoResult) => {
router.push(`/jo/edit?id=${record.id}`)
const getButtonSx = (jo : JobOrder) => { // TODO put it in ActionButtons.ts
const joStatus = jo.status?.toLowerCase();
const silStatus = jo.stockInLineStatus?.toLowerCase();
let btnSx = {label:"", color:""};
switch (joStatus) {
case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
// case "packaging":
// case "storing": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
case "storing":
switch (silStatus) {
case "pending": btnSx = {label: t("process epqc"), color:"primary.main"}; break;
case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
case "escalated":
if (sessionToken?.id == jo.silHandlerId) {
btnSx = {label: t("escalation processing"), color:"warning.main"};
break;
}
default: btnSx = {label: t("view stockin"), color:"info.main"};
}
break;
case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
default: btnSx = {label: t("scan picked material"), color:"success.main"};
}
return btnSx
};

const { data: session } = useSession();
const sessionToken = session as SessionWithTokens | null;

const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>();

const onDetailClick = useCallback((record: JobOrder) => {

if (record.status == "processing") {
handleUpdate(record)
} else if (record.status == "storing" || record.status == "completed") {
if (record.stockInLineId != null) {
const data = {
id: record.stockInLineId,
}
setModalInfo(data);
setOpenModal(true);
} else { alert('Invalid Stock In Line Id'); }
} else {
router.push(`/jo/edit?id=${record.id}`)
}
}, [])

const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first
// setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect
refetchData(defaultInputs, "search");
}, []);

const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
setInputs(() => ({
code: query.code,
name: query.name
itemName: query.itemName
}))
refetchData(query, "search");
}, [])
@@ -170,7 +284,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
onSearch={onSearch}
onReset={onReset}
/>
<SearchResults<SearchJoResult>
<SearchResults<JobOrder>
items={filteredJos}
columns={columns}
setPagingController={setPagingController}
@@ -184,6 +298,15 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
onClose={onCloseCreateJoModal}
onSearch={searchDataByPage}
/>

<QcStockInModal
session={sessionToken}
open={openModal}
onClose={closeNewModal}
inputDetail={modalInfo}
printerCombo={[]}
// skipQc={true}
/>
</>
}


+ 2
- 2
src/components/PoDetail/PutAwayForm.tsx Dosyayı Görüntüle

@@ -392,9 +392,9 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, setR
</Grid>
<Grid item xs={3}>
<TextField
label={t("acceptedQty")}
label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed
fullWidth
value={itemDetail.acceptedQty}
value={itemDetail.demandQty ?? itemDetail.acceptedQty}
disabled
/>
</Grid>


+ 8
- 3
src/components/PoDetail/QcComponent.tsx Dosyayı Görüntüle

@@ -104,6 +104,11 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
const [qcHistory, setQcHistory] = useState<PurchaseQcResult[]>([]);
const [qcResult, setQcResult] = useState<PurchaseQcResult[]>([]);

const detailMode = useMemo(() => {
const isDetailMode = itemDetail.status == "escalated" || isNaN(itemDetail.jobOrderId);
return isDetailMode;
}, [itemDetail]);

// const [qcAccept, setQcAccept] = useState(true);
// const [qcItems, setQcItems] = useState(dummyQCData)

@@ -610,7 +615,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
<FormControlLabel disabled={disabled}
value="1" control={<Radio />} label="接受來貨" />
{(itemDetail.status == "escalated"|| (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve
{(detailMode || (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve
<Box sx={{mr:2}}>
<TextField
// type="number"
@@ -661,7 +666,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
label={t("rejectQty")}
sx={{ width: '150px' }}
value={
(!Boolean(errors.acceptQty)) ?
(!Boolean(errors.acceptQty) && qcDecision !== undefined) ?
(qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty)
: ""
}
@@ -673,7 +678,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
<FormControlLabel disabled={disabled}
value="2" control={<Radio />}
sx={{"& .Mui-checked": {color: "red"}}}
label= {itemDetail.status == "escalated" ? "全部拒絕並退貨" : "不接受並退貨"} />
label= {detailMode ? "全部拒絕並退貨" : "不接受並退貨"} />
{(itemDetail.status == "pending" || disabled) && (<>
<FormControlLabel disabled={disabled}


+ 54
- 9
src/components/PoDetail/QcStockInModal.tsx Dosyayı Görüntüle

@@ -42,6 +42,7 @@ import { fetchStockInLineInfo } from "@/app/api/stockIn/actions";
import { fetchQcResult } from "@/app/api/qc/actions";
import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions";
import LoadingComponent from "../General/LoadingComponent";
import FgStockInForm from "../StockIn/FgStockInForm";


const style = {
@@ -64,6 +65,7 @@ interface CommonProps extends Omit<ModalProps, "children"> {
warehouse?: any[];
printerCombo: PrinterCombo[];
onClose: () => void;
skipQc?: Boolean;
}
interface Props extends CommonProps {
// itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
@@ -76,6 +78,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
session,
warehouse,
printerCombo,
skipQc = false,
}) => {
const {
t,
@@ -84,6 +87,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({

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

// Select Printer
@@ -110,6 +114,8 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
} else throw("Result is undefined");
} catch (e) {
console.log("%c Error when fetching Stock In Line: ", "color:red", e);
alert("Something went wrong, please retry");
closeHandler({}, "backdropClick");
}
},[fetchStockInLineInfo, inputDetail]
);
@@ -129,6 +135,8 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
// } else throw("Result is undefined");
} catch (e) {
console.log("%c Error when fetching Qc Result: ", "color:red", e);
// alert("Something went wrong, please retry");
// closeHandler({}, "backdropClick");
}
},[fetchQcResult]
);
@@ -145,6 +153,8 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
} else throw("Result is undefined");
} catch (e) {
console.log("%c Error when fetching EscalationLog: ", "color:red", e);
// alert("Something went wrong, please retry");
// closeHandler({}, "backdropClick");
}
},[fetchEscalationLogsByStockInLines]
);
@@ -334,10 +344,10 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
}
}
// Check if dates are input
if (data.productionDate === undefined || data.productionDate == null) {
alert("請輸入生產日期!");
return;
}
// if (data.productionDate === undefined || data.productionDate == null) {
// alert("請輸入生產日期!");
// return;
// }
if (data.expiryDate === undefined || data.expiryDate == null) {
alert("請輸入到期日!");
return;
@@ -357,7 +367,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
// validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`);
}

if (validationErrors.length > 0) {
if (validationErrors.length > 0 && !skipQc) {
console.error("QC Validation failed:", validationErrors);
alert(`未完成品檢: ${validationErrors}`);
return;
@@ -531,6 +541,25 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
// return isPassed
// }, [acceptQty, formProps])

const printQrcode = useCallback(
async () => {
setIsPrinting(true);
try {
const postData = { stockInLineIds: [stockInLineInfo?.id] };
const response = await fetchPoQrcode(postData);
if (response) {
console.log(response);
downloadFile(new Uint8Array(response.blobValue), response.filename!);
}
} catch (e) {
console.log("%c Error downloading QR Code", "color:red", e);
} finally {
setIsPrinting(false);
}
},
[stockInLineInfo],
);

return (
<>
<FormProvider {...formProps}>
@@ -577,12 +606,17 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
{t("Delivery Detail")}
</Typography>
</Grid>
<StockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} />
{stockInLineInfo.qcResult ?
{stockInLineInfo.jobOrderId ? (
<FgStockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} />
) : (
<StockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} />
)
}
{skipQc === false && (stockInLineInfo.qcResult ?
<QcComponent
itemDetail={stockInLineInfo}
disabled={viewOnly || showPutaway}
/> : <LoadingComponent/>
/> : <LoadingComponent/>)
}
<Stack direction="row" justifyContent="flex-end" gap={1} sx={{pt:2}}>
{(!viewOnly && !showPutaway) && (<Button
@@ -593,7 +627,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)}
>
{t("confirm qc result")}
{skipQc ? t("confirm") : t("confirm qc result")}
</Button>)}
</Stack>
</Box>
@@ -653,6 +687,17 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
>
{isPrinting ? t("Printing") : t("print")}
</Button>
<Button
id="demoPrint"
type="button"
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={printQrcode}
disabled={isPrinting}
>
{isPrinting ? t("downloading") : t("download Qr Code")}
</Button>
</Stack>
)}
</>) : <LoadingComponent/>}


+ 6
- 1
src/components/PutAwayScan/PutAwayModal.tsx Dosyayı Görüntüle

@@ -35,6 +35,7 @@ import { arrayToDateString, INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { msg } from "../Swal/CustomAlerts";
import { PutAwayRecord } from ".";
import FgStockInForm from "../StockIn/FgStockInForm";


interface Props extends Omit<ModalProps, "children"> {
@@ -337,7 +338,11 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId
處理上架
</Typography>
<Grid item xs={12}>
<StockInForm itemDetail={itemDetail} disabled={true} putawayMode={true}/>
{itemDetail.jobOrderId ? (
<FgStockInForm itemDetail={itemDetail} disabled={true} putawayMode={true}/>
) : (
<StockInForm itemDetail={itemDetail} disabled={true} putawayMode={true}/>
)}
</Grid>
<Paper sx={{ mt: 2, padding: 2, width: "100%", backgroundColor: verified ? '#bceb19' : '#FCD34D' }}>
<Grid


+ 442
- 0
src/components/StockIn/FgStockInForm.tsx Dosyayı Görüntüle

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

import {
PurchaseQcResult,
PurchaseQCInput,
StockInInput,
} from "@/app/api/stockIn/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/stockIn";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import dayjs from "dayjs";
// change PurchaseQcResult to stock in entry props
interface Props {
itemDetail: StockInLine;
// qc: QcItemWithChecks[];
disabled: boolean;
putawayMode?: boolean;
}
type EntryError =
| {
[field in keyof StockInInput]?: string;
}
| undefined;

// type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>;

const textfieldSx = {
width: "100%",
"& .MuiInputBase-root": {
// height: "120", // Scales with root font size
height: "5rem", // Scales with root font size
},
"& .MuiInputBase-input": {
height: "100%",
boxSizing: "border-box",
padding: "0.75rem",
fontSize: 24,
},
"& .MuiInputLabel-root": {
fontSize: 24,
transform: "translate(14px, 1.5rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 18,
transform: "translate(14px, -9px) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",
// transform: "translate(14px, 1.5rem) scale(1)",
// "&.MuiInputLabel-shrink": {
// fontSize: "0.875rem",
// },
// },
},
};

const FgStockInForm: React.FC<Props> = ({
// qc,
itemDetail,
disabled,
putawayMode = false,
}) => {
const {
t,
i18n: { language },
} = useTranslation("purchaseOrder");
const apiRef = useGridApiRef();
const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors,
} = useFormContext<StockInInput>();
// console.log(itemDetail);
useEffect(() => {
// console.log("triggered");
// // receiptDate default tdy
// setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT));
// setValue("status", "received");
}, [setValue]);

useEffect(() => {
console.log(errors);
}, [errors]);
const productionDate = watch("productionDate");
const expiryDate = watch("expiryDate");
const uom = watch("uom");

//// TODO : Add Checking ////
// Check if dates are input
// if (data.productionDate === undefined || data.productionDate == null) {
// validationErrors.push("請輸入生產日期!");
// }
// if (data.expiryDate === undefined || data.expiryDate == null) {
// validationErrors.push("請輸入到期日!");
// }

useEffect(() => {
// console.log(uom);
// console.log(productionDate);
// console.log(expiryDate);
if (expiryDate) clearErrors();
if (productionDate) clearErrors();
}, [productionDate, expiryDate, clearErrors]);

useEffect(() => {
console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail);
}, [itemDetail]);

return (
<Grid container justifyContent="flex-start" alignItems="flex-start">
{/* <Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Stock In Detail")}
</Typography>
</Grid> */}
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
{putawayMode && (
<Grid item xs={6}>
<TextField
label={t("joCode")}
fullWidth
{...register("productLotNo", {
// required: "productLotNo required!",
})}
sx={textfieldSx}
disabled={true}
/>
</Grid>
)
}
<Grid item xs={6}>
<TextField
label={t("itemName")}
fullWidth
{...register("itemName", {
// required: "productLotNo required!",
})}
sx={textfieldSx}
disabled={true}
// error={Boolean(errors.productLotNo)}
// helperText={errors.productLotNo?.message}
/>
</Grid>
{putawayMode || (
<>
<Grid item xs={6}>
<Controller
control={control}
name="receiptDate"
rules={{ required: true }}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={textfieldSx}
label={t("receiptDate")}
value={dayjs(watch("receiptDate"))}
disabled={true}
onChange={(date) => {
if (!date) return;
// setValue("receiptDate", date.format(INPUT_DATE_FORMAT));
field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.receiptDate?.message),
helperText: errors.receiptDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
</>
)}
<Grid item xs={6}>
{putawayMode ? (
<TextField
label={t("stockLotNo")}
fullWidth
{...register("lotNo", {
// required: "productLotNo required!",
})}
sx={textfieldSx}
disabled={true}
error={Boolean(errors.productLotNo)}
helperText={errors.productLotNo?.message}
/>) : (

<TextField
label={t("stockLotNo")}
fullWidth
{...register("productLotNo", {
// required: "productLotNo required!",
})}
sx={textfieldSx}
disabled={true}
error={Boolean(errors.productLotNo)}
helperText={errors.productLotNo?.message}
/>)
}
</Grid>
{/* {putawayMode || (<>
<Grid item xs={6}>
<Controller
control={control}
name="productionDate"
// rules={{ required: !Boolean(expiryDate) }}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={textfieldSx}
label={t("productionDate")}
value={productionDate ? dayjs(productionDate) : undefined}
disabled={disabled}
onChange={(date) => {
if (!date) return;
setValue(
"productionDate",
date.format(INPUT_DATE_FORMAT),
);
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.productionDate?.message),
helperText: errors.productionDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("qty")}
fullWidth
{...register("qty", {
required: "qty required!",
})}
sx={textfieldSx}
disabled={true}
/>
</Grid></>
)} */}
<Grid item xs={6}>
<Controller
control={control}
name="expiryDate"
// rules={{ required: !Boolean(productionDate) }}
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={textfieldSx}
label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined}
disabled={disabled}
onChange={(date) => {
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.expiryDate?.message),
helperText: errors.expiryDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
{/* <Grid item xs={6}>
{putawayMode ? (
<TextField
label={t("acceptedQty")}
fullWidth
sx={textfieldSx}
disabled={true}
value={itemDetail.acceptedQty}
// disabled={true}
// disabled={disabled}
// error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message}
/>
) : (
<TextField
label={t("receivedQty")}
fullWidth
{...register("receivedQty", {
required: "receivedQty required!",
})}
sx={textfieldSx}
disabled={true}
/>
)}
</Grid> */}
<Grid item xs={6}>
<TextField
label={t("salesUnit")}
fullWidth
{...register("uom.udfudesc", {
required: "uom required!",
})}
// value={uom?.code}
sx={textfieldSx}
disabled={true}
/>
</Grid>
<Grid item xs={6}>
{putawayMode ? (
<TextField
label={t("processedQty")}
fullWidth
sx={textfieldSx}
disabled={true}
value={itemDetail.putAwayLines?.reduce((sum, p) => sum + p.qty, 0) ?? 0}
// disabled={true}
// disabled={disabled}
// error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message}
/>
) : (
<TextField
label={t("acceptedQty")}
fullWidth
sx={textfieldSx}
disabled={true}
{...register("acceptedQty", {
required: "acceptedQty required!",
})}
// disabled={true}
// disabled={disabled}
// error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message}
/>
)}
</Grid>
{/* <Grid item xs={4}>
<TextField
label={t("acceptedWeight")}
fullWidth
// {...register("acceptedWeight", {
// required: "acceptedWeight required!",
// })}
disabled={disabled}
error={Boolean(errors.acceptedWeight)}
helperText={errors.acceptedWeight?.message}
/>
</Grid> */}
</Grid>
<Grid
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
sx={{ mt: 0.5 }}
>
{/* <Grid item xs={12}>
<InputDataGrid<PurchaseQCInput, PurchaseQcResult, EntryError>
apiRef={apiRef}
checkboxSelection={false}
_formKey={"qcCheck"}
columns={columns}
validateRow={validationTest}
/>
</Grid> */}
</Grid>
</Grid>
);
};
export default FgStockInForm;

+ 16
- 4
src/i18n/zh/jo.json Dosyayı Görüntüle

@@ -3,25 +3,37 @@
"Create Job Order": "創建工單",
"Edit Job Order Detail": "編輯工單",
"Details": "細節",
"Actions": "操作",
"Code": "工單編號",
"Name": "成品/半成品名稱",
"Picked Qty": "已提料數量",
"Req. Qty": "需求數量",
"UoM": "銷售單位",
"Status": "來貨狀態",
"Status": "工單狀態",
"Lot No.": "批號",
"Bom": "物料清單",
"Release": "放單",
"Pending": "待掃碼",
"Pending for pick": "待提料",
"Planning": "計劃中",
"Scanned": "已掃碼",
"Processing": "已開始工序",
"Storing": "入倉中",
"Storing": "成品入倉中",
"completed": "已完成",
"Completed": "已完成",
"Cancel": "取消",
"Create": "創建",

"view stockin": "查看入倉詳情",
"view putaway": "查看上架詳情",
"process epqc": "進行成品檢驗",
"scan picked material": "掃碼確認提料",
"escalation processing": "處理上報記錄",
"process stockIn": "進行收貨程序",
"release jo": "確認發佈工單",
"complete jo": "完成工單",
"update success": "成功更新資料",

"Scanned": "已掃碼",
"Scan Status": "掃碼狀態",
"Start Job Order": "開始工單",
"Target Production Date" : "預計生產日期",
@@ -80,7 +92,7 @@
"Scanning...": "掃碼中",
"Unassigned Job Orders": "未分配工單",
"Please scan the item qr code": "請掃描物料二維碼",
"Please make sure the qty is enough": "請確保物料數量是足夠",
"Please make sure the qty is enough": "物料數量不足",
"Please make sure all required items are picked": "請確保所有物料已被提取",
"Do you want to start job order": "是否開始工單",
"Submit": "提交",


+ 6
- 1
src/i18n/zh/purchaseOrder.json Dosyayı Görüntüle

@@ -45,6 +45,7 @@
"processedQty": "已上架數量",
"expiryDate": "到期日",
"acceptedQty": "本批收貨數量",
"acceptedPutawayQty": "本批上架數量",
"putawayQty": "上架數量",
"acceptQty": "揀收數量",
"printQty": "列印數量",
@@ -151,5 +152,9 @@
"Qc Decision": "品檢詳情",
"Print Qty": "列印數量",
"putawayDatetime": "上架時間",
"putawayUser": "上架同事"
"putawayUser": "上架同事",
"joCode": "工單編號",
"salesUnit": "銷售單位",
"download Qr Code": "下載QR碼",
"downloading": "下載中"
}

Yükleniyor…
İptal
Kaydet