| @@ -1,11 +1,12 @@ | |||||
| "use server"; | "use server"; | ||||
| import { cache } from 'react'; | import { cache } from 'react'; | ||||
| import { Pageable, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
| import { Pageable, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
| import { JobOrder, JoStatus, Machine, Operator } from "."; | import { JobOrder, JoStatus, Machine, Operator } from "."; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | ||||
| import { FileResponse } from "@/app/api/pdf/actions"; | |||||
| export interface SaveJo { | export interface SaveJo { | ||||
| bomId: number; | bomId: number; | ||||
| @@ -155,7 +156,7 @@ export const printFGStockInLabel = cache(async(data: PrintFGStockInLabelRequest) | |||||
| } | } | ||||
| return serverFetchWithNoContent( | return serverFetchWithNoContent( | ||||
| `${BASE_API_URL}/jo/print-FGPickRecordLabel?${params.toString()}`, | |||||
| `${BASE_API_URL}/jo/print-FGStockInLabel?${params.toString()}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| next: { | next: { | ||||
| @@ -1027,3 +1028,20 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ | |||||
| return { success: true, message: "Print job sent successfully (Pick Record)" } as PrintPickRecordResponse; | return { success: true, message: "Print job sent successfully (Pick Record)" } as PrintPickRecordResponse; | ||||
| } | } | ||||
| export interface ExportFGStockInLabelRequest { | |||||
| stockInLineId: number; | |||||
| } | |||||
| export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Promise<FileResponse> => { | |||||
| const reportBlob = await serverFetchBlob<FileResponse>( | |||||
| `${BASE_API_URL}/jo/FGStockInLabel`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| return reportBlob; | |||||
| }; | |||||
| @@ -94,7 +94,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| setModalInfo({ | setModalInfo({ | ||||
| id: process.stockInLineId, | id: process.stockInLineId, | ||||
| expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), | |||||
| //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT), | |||||
| // 视需要补 itemId、jobOrderId 等 | // 视需要补 itemId、jobOrderId 等 | ||||
| }); | }); | ||||
| setOpenModal(true); | setOpenModal(true); | ||||
| @@ -413,10 +413,11 @@ useEffect(() => { | |||||
| } else { return 60} | } else { return 60} | ||||
| }; | }; | ||||
| const formattedDesc = (content: string = "") => { | |||||
| const formattedDesc = (content: string | null | undefined = "") => { | |||||
| const safeContent = content || ""; | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {content.split("\\n").map((line, index) => ( | |||||
| {safeContent.split("\\n").map((line, index) => ( | |||||
| <span key={index}> {line} <br/></span> | <span key={index}> {line} <br/></span> | ||||
| ))} | ))} | ||||
| </> | </> | ||||
| @@ -40,7 +40,7 @@ import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForS | |||||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | ||||
| import FgStockInForm from "../StockIn/FgStockInForm"; | import FgStockInForm from "../StockIn/FgStockInForm"; | ||||
| import LoadingComponent from "../General/LoadingComponent"; | import LoadingComponent from "../General/LoadingComponent"; | ||||
| import { printFGStockInLabel, PrintFGStockInLabelRequest } from "@/app/api/jo/actions"; | |||||
| import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions"; | |||||
| const style = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| @@ -119,7 +119,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| const res = await fetchStockInLineInfo(stockInLineId); | const res = await fetchStockInLineInfo(stockInLineId); | ||||
| if (res) { | if (res) { | ||||
| console.log("%c Fetched Stock In Line: ", "color:orange", res); | console.log("%c Fetched Stock In Line: ", "color:orange", res); | ||||
| setStockInLineInfo({...inputDetail, ...res, expiryDate: inputDetail?.expiryDate}); // TODO review to overwrite res with inputDetail instead (revise PO fetching data) | |||||
| setStockInLineInfo({...inputDetail, ...res, expiryDate: res.expiryDate}); | |||||
| // fetchQcResultData(stockInLineId); | // fetchQcResultData(stockInLineId); | ||||
| } else throw("Result is undefined"); | } else throw("Result is undefined"); | ||||
| @@ -168,8 +168,8 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| { | { | ||||
| ...d, | ...d, | ||||
| // status: d.status ?? "pending", | // status: d.status ?? "pending", | ||||
| productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : undefined, | |||||
| expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined, | |||||
| productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : dayjs().format(INPUT_DATE_FORMAT), | |||||
| expiryDate: d.expiryDate ? (Array.isArray(d.expiryDate) ? arrayToDateString(d.expiryDate, "input") : d.expiryDate) : undefined, | |||||
| receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") | receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") | ||||
| : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | ||||
| acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0, | acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0, | ||||
| @@ -350,9 +350,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| const qcData = { | const qcData = { | ||||
| dnNo : data.dnNo? data.dnNo : "DN00000", | dnNo : data.dnNo? data.dnNo : "DN00000", | ||||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | ||||
| productionDate : arrayToDateString(data.productionDate, "input"), | |||||
| expiryDate : arrayToDateString(data.expiryDate, "input"), | |||||
| receiptDate : arrayToDateString(data.receiptDate, "input"), | |||||
| productionDate : data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, | |||||
| expiryDate : data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined, | |||||
| receiptDate : data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, | |||||
| qcAccept: qcAccept? qcAccept : false, | qcAccept: qcAccept? qcAccept : false, | ||||
| acceptQty: acceptQty? acceptQty : 0, | acceptQty: acceptQty? acceptQty : 0, | ||||
| @@ -540,24 +540,38 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| // return isPassed | // return isPassed | ||||
| // }, [acceptQty, formProps]) | // }, [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 { | |||||
| const printQrcode = useCallback( | |||||
| async () => { | |||||
| setIsPrinting(true); | |||||
| try { | |||||
| let response; | |||||
| if (printSource === "productionProcess") { | |||||
| // Use FG Stock In Label download API for production process | |||||
| if (!stockInLineInfo?.id) { | |||||
| console.error("Stock In Line ID is required for download"); | |||||
| setIsPrinting(false); | setIsPrinting(false); | ||||
| return; | |||||
| } | } | ||||
| }, | |||||
| [stockInLineInfo], | |||||
| ); | |||||
| const postData = { stockInLineId: stockInLineInfo.id }; | |||||
| response = await fetchFGStockInLabel(postData); | |||||
| } else { | |||||
| const postData = { stockInLineIds: [stockInLineInfo?.id] }; | |||||
| 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, printSource], | |||||
| ); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -12,11 +12,12 @@ import { | |||||
| TextField, | TextField, | ||||
| Tooltip, | Tooltip, | ||||
| Typography, | Typography, | ||||
| Button, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Controller, useFormContext } from "react-hook-form"; | import { Controller, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { useCallback, useEffect, useMemo } from "react"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | import { | ||||
| GridColDef, | GridColDef, | ||||
| GridRowIdGetter, | GridRowIdGetter, | ||||
| @@ -35,6 +36,11 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import CalculateExpiryDateModal from "./CalculateExpiryDateModal"; | |||||
| import { InputAdornment } from "@mui/material"; | |||||
| import { dayjsToDateString } from "@/app/utils/formatUtil"; | |||||
| // change PurchaseQcResult to stock in entry props | // change PurchaseQcResult to stock in entry props | ||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | itemDetail: StockInLine; | ||||
| @@ -115,6 +121,8 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| console.log(errors); | console.log(errors); | ||||
| }, [errors]); | }, [errors]); | ||||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||||
| const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false); | |||||
| const productionDate = watch("productionDate"); | const productionDate = watch("productionDate"); | ||||
| const expiryDate = watch("expiryDate"); | const expiryDate = watch("expiryDate"); | ||||
| const uom = watch("uom"); | const uom = watch("uom"); | ||||
| @@ -140,7 +148,23 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | ||||
| }, [itemDetail]); | }, [itemDetail]); | ||||
| return ( | |||||
| const handleOpenModal = useCallback(() => { | |||||
| setOpenModal(true); | |||||
| }, []); | |||||
| const handleOnModalClose = useCallback(() => { | |||||
| setOpenExpDatePicker(false); | |||||
| setOpenModal(false); | |||||
| }, []); | |||||
| const handleReturnExpiryDate = useCallback((result: dayjs.Dayjs) => { | |||||
| if (result) { | |||||
| setValue("expiryDate", dayjsToDateString(result)); | |||||
| } | |||||
| }, [setValue]); | |||||
| return ( | |||||
| <> | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | <Grid container justifyContent="flex-start" alignItems="flex-start"> | ||||
| {/* <Grid item xs={12}> | {/* <Grid item xs={12}> | ||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | <Typography variant="h6" display="block" marginBlockEnd={1}> | ||||
| @@ -250,6 +274,44 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| />) | />) | ||||
| } | } | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="productionDate" | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={textfieldSx} | |||||
| label={t("productionDate")} | |||||
| value={productionDate ? dayjs(productionDate) : undefined} | |||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| if (!date) return; | |||||
| setValue( | |||||
| "productionDate", | |||||
| date.format(INPUT_DATE_FORMAT), | |||||
| ); | |||||
| }} | |||||
| inputRef={field.ref} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| error: Boolean(errors.productionDate?.message), | |||||
| helperText: errors.productionDate?.message, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| {/* {putawayMode || (<> | |||||
| {/* {putawayMode || (<> | {/* {putawayMode || (<> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <Controller | <Controller | ||||
| @@ -313,28 +375,47 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| dateAdapter={AdapterDayjs} | dateAdapter={AdapterDayjs} | ||||
| adapterLocale={`${language}-hk`} | adapterLocale={`${language}-hk`} | ||||
| > | > | ||||
| <DatePicker | |||||
| {...field} | |||||
| sx={textfieldSx} | |||||
| label={t("expiryDate")} | |||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||||
| format={OUTPUT_DATE_FORMAT} | |||||
| 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, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={textfieldSx} | |||||
| label={t("expiryDate")} | |||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| if (!date) return; | |||||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||||
| setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); | |||||
| }} | |||||
| inputRef={field.ref} | |||||
| open={openExpDatePicker && !openModal} | |||||
| onOpen={() => setOpenExpDatePicker(true)} | |||||
| onClose={() => setOpenExpDatePicker(false)} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| InputProps: { | |||||
| ...(!disabled && { | |||||
| endAdornment: ( | |||||
| <InputAdornment position='end'> | |||||
| <Button | |||||
| type="button" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| sx={{ fontSize: '24px' }} | |||||
| onClick={handleOpenModal} | |||||
| > | |||||
| {t("Calculate Expiry Date")} | |||||
| </Button> | |||||
| </InputAdornment> | |||||
| ), | |||||
| }) | |||||
| }, | |||||
| error: Boolean(errors.expiryDate?.message), | |||||
| helperText: errors.expiryDate?.message, | |||||
| onClick: () => setOpenExpDatePicker(true), | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | </LocalizationProvider> | ||||
| ); | ); | ||||
| }} | }} | ||||
| @@ -442,6 +523,13 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| </Grid> */} | </Grid> */} | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| ); | |||||
| <CalculateExpiryDateModal | |||||
| open={openModal} | |||||
| onClose={handleOnModalClose} | |||||
| onSubmit={handleReturnExpiryDate} | |||||
| textfieldSx={textfieldSx} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | }; | ||||
| export default FgStockInForm; | export default FgStockInForm; | ||||