diff --git a/src/app/(main)/po/page.tsx b/src/app/(main)/po/page.tsx index f9f33d9..61b5dfe 100644 --- a/src/app/(main)/po/page.tsx +++ b/src/app/(main)/po/page.tsx @@ -17,7 +17,7 @@ const PurchaseOrder: React.FC = async () => { // preloadClaims(); return ( <> - + void; + onSubmit: (result: Dayjs) => void; + textfieldSx?: any; +} + +type EntryError = + { + productionDate: string, + expiryDate: string, + shelfLife: string, + } + +const CalculateExpiryDateModal: React.FC = ({ + open, + onClose, + onSubmit, + textfieldSx, +}) => { + const { t, i18n: { language }, } = useTranslation("purchaseOrder"); + + const [productionDate, setProductionDate] = useState(); + const [shelfLife, setShelfLife] = useState(); + const [expiryDate, setExpiryDate] = useState(); + const [errors, setErrors] = useState({ productionDate: "", expiryDate: "", shelfLife: "" }); + + const onModalClose = useCallback(() => { + setProductionDate(undefined); + setExpiryDate(undefined); + setShelfLife(undefined); + onClose(); + }, []) + + const handleSubmit = useCallback(() => { + if (expiryDate) { + onSubmit(expiryDate); + } + onModalClose(); + }, [onModalClose, expiryDate]) + + const hasError = useMemo(() => { + return Object.values(errors).some(err => err.length > 0); + }, [errors]) + + // Validation + useEffect(() => { + let err = { productionDate: "", expiryDate: "", shelfLife: "" }; + + // Check Expiry Date + if (expiryDate !== undefined) { + // Check Validity (If allow input for Expiry Date) + // if (!expiryDate.isValid()) { err = ({...err, expiryDate: t("Invalid Date")}); } else + // Check date logic + if (shelfLife === undefined && productionDate && expiryDate.isBefore(productionDate)) { + err = ({...err, expiryDate: t("Expiry Date cannot be earlier than Production Date")}); + } + } + + // Check Production Date + if (productionDate !== undefined) { + // Check Validity + if (!productionDate.isValid()) { err = ({...err, productionDate: t("Invalid Date")}); } + // Check date logic + else if (shelfLife === undefined && expiryDate && productionDate.isAfter(expiryDate)) { + err = ({...err, productionDate: t("Production Date must be earlier than Expiry Date")}); + } + } + + // Check Shelf Life + // if (shelfLife === undefined) { + // err = ({...err, shelfLife: t("Shelf Life must be greater than 0")}); + // } + + setErrors(err); + }, [expiryDate, productionDate, shelfLife]); + + const calculateDates = useCallback((value: Dayjs | number | null = null, + valueType: "productionDate" | "shelfLife" | "expiryDate" | undefined = undefined) => { + + let pd = productionDate; + let ed = expiryDate; + let sl = shelfLife; + + if (value !== null && value !== undefined) { + if (dayjs.isDayjs(value)) { + if (valueType == "expiryDate") { + ed = value; + } else { + pd = value; + } + } else if (!isNaN(value)) { + sl = value; + } else { + console.log("%c Invalid input value for calculation", "color:red", value); + return; + } + } else { + if (valueType !== undefined) { + ed = undefined; // Clear Expiry Date if fields are cleared + switch (valueType) { + case "productionDate": pd = undefined; break; + case "shelfLife": sl = undefined; break; + // case "expiryDate": ed = undefined; break; + default: console.log("%c Invalid value type for calculation", "color:red", valueType); return; + } + } + } + + if (pd && ed && sl !== undefined) { // If all values are set + if (valueType == "expiryDate") { // If updating Expiry Date -> Change Production Date + const result = ed.add(sl * -1, "day"); + setProductionDate(result); + } else { // If not updating Expiry Date -> Change Expiry Date + const result = pd.add(sl, "day"); + setExpiryDate(result); + } + + } else if (pd && sl !== undefined) { // Set Expiry Date + const result = pd.add(sl, "day"); + if (result.isValid()) { setExpiryDate(result); } + } else if (sl !== undefined && ed) { // Set Production Date + const result = ed.add(sl * -1, "day"); + if (result.isValid()) { setProductionDate(result); } + } else if (pd && ed) { // Set Shelf Life + const result = ed.diff(pd, "day"); + if (!isNaN(result) && result > 0) { setShelfLife(result); } + } else { + setExpiryDate(undefined); + } + }, [productionDate, shelfLife, expiryDate]); + + return ( + + + + + + + {t("Fill in Expiry Date")} + + + + + + { + if (!date) { + setProductionDate(undefined); + } else { + setProductionDate(date); + } + calculateDates(date, "productionDate"); + }} + slotProps={{ + textField: { + error: errors.productionDate.length > 0, + helperText: errors.productionDate, + }, + }} + /> + + + + + + { + const val = value == 0 ? undefined : value; + setShelfLife(val); + calculateDates(val, "shelfLife"); + }} + /> + + + + + + + { + if (!date) { + setExpiryDate(undefined); + } else { + setExpiryDate(date); + calculateDates(date, "expiryDate"); + } + }} + slotProps={{ + textField: { + error: errors.expiryDate.length > 0, + helperText: errors.expiryDate, + }, + }} + disabled={true} + /> + + + + + + + + + + + + ) +} + +export default CalculateExpiryDateModal; \ No newline at end of file diff --git a/src/components/StockIn/FgStockInForm.tsx b/src/components/StockIn/FgStockInForm.tsx index c6ff5b6..af46970 100644 --- a/src/components/StockIn/FgStockInForm.tsx +++ b/src/components/StockIn/FgStockInForm.tsx @@ -33,7 +33,7 @@ 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 { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import dayjs from "dayjs"; // change PurchaseQcResult to stock in entry props interface Props { @@ -60,14 +60,14 @@ const textfieldSx = { height: "100%", boxSizing: "border-box", padding: "0.75rem", - fontSize: 24, + fontSize: 30, }, "& .MuiInputLabel-root": { - fontSize: 24, - transform: "translate(14px, 1.5rem) scale(1)", + fontSize: 30, + transform: "translate(14px, 1.2rem) scale(1)", "&.MuiInputLabel-shrink": { - fontSize: 18, - transform: "translate(14px, -9px) scale(1)", + fontSize: 24, + transform: "translate(14px, -0.5rem) scale(1)", }, // [theme.breakpoints.down("sm")]: { // fontSize: "1rem", @@ -200,6 +200,7 @@ const FgStockInForm: React.FC = ({ sx={textfieldSx} label={t("receiptDate")} value={dayjs(watch("receiptDate"))} + format={OUTPUT_DATE_FORMAT} disabled={true} onChange={(date) => { if (!date) return; @@ -317,6 +318,7 @@ const FgStockInForm: React.FC = ({ sx={textfieldSx} label={t("expiryDate")} value={expiryDate ? dayjs(expiryDate) : undefined} + format={OUTPUT_DATE_FORMAT} disabled={disabled} onChange={(date) => { if (!date) return; @@ -375,20 +377,29 @@ const FgStockInForm: React.FC = ({ disabled={true} /> - - {putawayMode ? ( + {putawayMode ? (<> + sum + p.qty, 0) ?? 0} - // disabled={true} - // disabled={disabled} - // error={Boolean(errors.acceptedQty)} - // helperText={errors.acceptedQty?.message} /> + + + + ) : ( + = ({ {...register("acceptedQty", { required: "acceptedQty required!", })} - // disabled={true} - // disabled={disabled} - // error={Boolean(errors.acceptedQty)} - // helperText={errors.acceptedQty?.message} /> - )} - + + )} {/* void; + label?: string; + sx?: any; +} + +const ShelfLifeContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'flex-start', + width: '100%', + '& .MuiTextField-root': { + flex: 1, + '& input': { + textAlign: 'center', + }, + }, +})); + +const daysToDuration = (totalDays: number) => { + const years = Math.floor(totalDays / 365); + const remainingDaysAfterYears = totalDays % 365; + const months = Math.floor(remainingDaysAfterYears / 30); // **This is Approximate months (Not according to locale) + const days = remainingDaysAfterYears % 30; + return { years, months, days }; +}; + +const durationToDays = (years: number, months: number, days: number) => { + return years * 365 + months * 30 + days; +}; + +// Format duration to readable string with proper roll-over (Assuming 30 days per month) +const formatDuration = (years: number, months: number, days: number) => { + let adjustedYears = years; + let adjustedMonths = months; + let adjustedDays = days; + + // Roll over days to months + if (days >= 30) { + adjustedMonths += Math.floor(days / 30); + adjustedDays = days % 30; + } + + // Roll over months to years + if (adjustedMonths >= 12) { + adjustedYears += Math.floor(adjustedMonths / 12); + adjustedMonths = adjustedMonths % 12; + } + + const parts: string[] = []; + if (adjustedYears > 0) parts.push(`${adjustedYears} 年`); + if (adjustedMonths > 0) parts.push(`${adjustedMonths} 個月 ${adjustedDays > 0 ? ' + ' : ''}`); + if (adjustedDays > 0) parts.push(`${adjustedDays} 日`); + return parts.length > 0 ? parts.join(' ') : '0 日'; +}; + +const ShelfLifeInput: React.FC = ({ value = 0, onChange = () => {}, label = 'Shelf Life', sx }) => { + const { t } = useTranslation("purchaseOrder"); + + const { years, months, days } = daysToDuration(value); + const [duration, setDuration] = useState({ + years: years || 0, + months: months || 0, + days: days || 0, + }); + + const totalDays = useMemo(() => { + return durationToDays(duration.years, duration.months, duration.days); + }, [duration]); + + useEffect(() => { + onChange(totalDays); + }, [totalDays]); + + // Set value when value prop changes + useEffect(() => { + if (value != totalDays) { + setDuration(daysToDuration(value)); + } + }, [value]); + + // Handle input changes + const handleChange = (field: 'years' | 'months' | 'days') => ( + event: React.ChangeEvent + ) => { + const input = event.target.value; + // Allow only non-negative integers + if (input === '' || (/^\d+$/.test(input) && parseInt(input) >= 0)) { + setDuration((prev) => ({ + ...prev, + [field]: input === '' ? 0 : parseInt(input), + })); + } + }; + + return ( + + + + + + + + {label}: + {/* {formatDuration(duration.years, duration.months, duration.days)} */} + {totalDays} 日 + + + + ); +}; + +export default ShelfLifeInput; \ No newline at end of file diff --git a/src/components/StockIn/StockInForm.tsx b/src/components/StockIn/StockInForm.tsx index 2205adf..8b88692 100644 --- a/src/components/StockIn/StockInForm.tsx +++ b/src/components/StockIn/StockInForm.tsx @@ -5,9 +5,11 @@ import { } from "@/app/api/stockIn/actions"; import { Box, + Button, Card, CardContent, Grid, + InputAdornment, Stack, TextField, Tooltip, @@ -16,25 +18,16 @@ import { import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useState, 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 { dayjsToDateString, INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import dayjs from "dayjs"; +import CalculateExpiryDateModal from "./CalculateExpiryDateModal"; // change PurchaseQcResult to stock in entry props interface Props { itemDetail: StockInLine; @@ -60,14 +53,14 @@ const textfieldSx = { height: "100%", boxSizing: "border-box", padding: "0.75rem", - fontSize: 24, + fontSize: 30, }, "& .MuiInputLabel-root": { - fontSize: 24, - transform: "translate(14px, 1.5rem) scale(1)", + fontSize: 30, + transform: "translate(14px, 1.2rem) scale(1)", "&.MuiInputLabel-shrink": { - fontSize: 18, - transform: "translate(14px, -9px) scale(1)", + fontSize: 24, + transform: "translate(14px, -0.5rem) scale(1)", }, // [theme.breakpoints.down("sm")]: { // fontSize: "1rem", @@ -119,11 +112,11 @@ const StockInForm: React.FC = ({ const expiryDate = watch("expiryDate"); const uom = watch("uom"); + const [openModal, setOpenModal] = useState(false); + const [openExpDatePicker, setOpenExpDatePicker] = useState(false); + //// 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("請輸入到期日!"); // } @@ -133,14 +126,29 @@ const StockInForm: React.FC = ({ // 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]); + const handleOpenModal = useCallback(() => { + setOpenModal(true); + }, []); + + const handleOnModalClose = useCallback(() => { + setOpenExpDatePicker(false); + setOpenModal(false); + }, []); + + const handleReturnExpiryDate = useCallback((result: dayjs.Dayjs) => { + if (result) { + setValue("expiryDate", dayjsToDateString(result)); + } + }, []); + return ( + <> {/* @@ -211,6 +219,7 @@ const StockInForm: React.FC = ({ sx={textfieldSx} label={t("receiptDate")} value={dayjs(watch("receiptDate"))} + format={OUTPUT_DATE_FORMAT} disabled={true} onChange={(date) => { if (!date) return; @@ -270,58 +279,6 @@ const StockInForm: React.FC = ({ helperText={errors.productLotNo?.message} />)} - {putawayMode || (<> - - { - return ( - - { - 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, - }, - }} - /> - - ); - }} - /> - - - - - )} = ({ sx={textfieldSx} label={t("expiryDate")} value={expiryDate ? dayjs(expiryDate) : undefined} + format={OUTPUT_DATE_FORMAT} disabled={disabled} onChange={(date) => { if (!date) return; @@ -346,11 +304,30 @@ const StockInForm: React.FC = ({ // field.onChange(date); }} inputRef={field.ref} + open={openExpDatePicker && !openModal} + onOpen={() => setOpenExpDatePicker(true)} + onClose={() => setOpenExpDatePicker(false)} slotProps={{ textField: { + InputProps: { + ...(!disabled && {endAdornment: ( + + + + ),}) + }, // required: true, error: Boolean(errors.expiryDate?.message), helperText: errors.expiryDate?.message, + onClick: () => setOpenExpDatePicker(true), }, }} /> @@ -359,31 +336,45 @@ const StockInForm: React.FC = ({ }} /> - - {putawayMode ? ( + {putawayMode || (<> + - ) : ( + + - )} - + + )} + {putawayMode && ( + + + + )} = ({ */} + + + ); }; export default StockInForm; diff --git a/src/i18n/zh/items.json b/src/i18n/zh/items.json index 40fcdcf..1c52823 100644 --- a/src/i18n/zh/items.json +++ b/src/i18n/zh/items.json @@ -15,7 +15,7 @@ "name": "名稱", "description": "描述", "Type": "類型", -"shelfLife": "保存期限", +"shelfLife": "保質期", "remarks": "備註", "countryOfOrigin": "原產地", "maxQty": "最大數量", diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index 9900803..806a1e8 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -157,5 +157,12 @@ "salesUnit": "銷售單位", "download Qr Code": "下載QR碼", "downloading": "下載中", - "&": "及" + "&": "及", + "Calculate Expiry Date": "沒有到期日,填寫生產日期及保質期", + "shelfLife": "保質期", + "Fill in Expiry Date": "請填寫生產日期及保質期,以計算到期日", + "Expiry Date cannot be earlier than Production Date": "到期日不可早於生產日期", + "Production Date must be earlier than Expiry Date": "生產日期必須早於到期日", + "confirm expiry date": "確認到期日", + "Invalid Date": "無效日期" }