| @@ -17,7 +17,7 @@ const PurchaseOrder: React.FC = async () => { | |||
| // preloadClaims(); | |||
| return ( | |||
| <> | |||
| <I18nProvider namespaces={["purchaseOrder", "common"]}> | |||
| <I18nProvider namespaces={["purchaseOrder", "common", "items"]}> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| @@ -0,0 +1,299 @@ | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { Check, SwapHoriz, Add } from "@mui/icons-material"; | |||
| import { Box, Button, Card, Grid, Modal, Stack, TextField, Typography } from "@mui/material"; | |||
| import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { msg } from "../Swal/CustomAlerts"; | |||
| import ShelfLifeInput from "./ShelfLifeInput"; | |||
| interface Props { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| onSubmit: (result: Dayjs) => void; | |||
| textfieldSx?: any; | |||
| } | |||
| type EntryError = | |||
| { | |||
| productionDate: string, | |||
| expiryDate: string, | |||
| shelfLife: string, | |||
| } | |||
| const CalculateExpiryDateModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| onSubmit, | |||
| textfieldSx, | |||
| }) => { | |||
| const { t, i18n: { language }, } = useTranslation("purchaseOrder"); | |||
| const [productionDate, setProductionDate] = useState<Dayjs>(); | |||
| const [shelfLife, setShelfLife] = useState<number>(); | |||
| const [expiryDate, setExpiryDate] = useState<Dayjs>(); | |||
| const [errors, setErrors] = useState<EntryError>({ 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 ( | |||
| <Modal | |||
| open={open} | |||
| onClose={onModalClose} | |||
| > | |||
| <Card | |||
| style={{ | |||
| flex: 10, | |||
| marginBottom: "20px", | |||
| width: "80%", | |||
| // height: "80%", | |||
| position: "fixed", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: "block", | |||
| "flex-direction": "column", | |||
| padding: "20px", | |||
| height: "100%", //'30rem', | |||
| width: "100%", | |||
| "& .actions": { | |||
| color: "text.secondary", | |||
| }, | |||
| "& .header": { | |||
| }, | |||
| "& .textPrimary": { | |||
| color: "text.primary", | |||
| }, | |||
| }} | |||
| > | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <Stack sx={{mb: 1}}> | |||
| <Typography variant="h4" component="h2" sx={{ fontWeight: 'bold', mb: 2 }}> | |||
| {t("Fill in Expiry Date")} | |||
| </Typography> | |||
| </Stack> | |||
| <Stack> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| > | |||
| <Grid item xs={2.5}> | |||
| <DatePicker | |||
| sx={textfieldSx} | |||
| label={t("productionDate")} | |||
| value={productionDate ? dayjs(productionDate) : null} | |||
| format={OUTPUT_DATE_FORMAT} | |||
| onChange={(date) => { | |||
| if (!date) { | |||
| setProductionDate(undefined); | |||
| } else { | |||
| setProductionDate(date); | |||
| } | |||
| calculateDates(date, "productionDate"); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| error: errors.productionDate.length > 0, | |||
| helperText: errors.productionDate, | |||
| }, | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={0.5} sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| height: '100%', | |||
| }}> | |||
| <Add sx={{ | |||
| fontSize: '60px', color:'secondary.main', | |||
| transform: 'translate(10%, 15%)', | |||
| }}/> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <ShelfLifeInput | |||
| sx={textfieldSx} | |||
| value={shelfLife} | |||
| label={t("shelfLife")} | |||
| onChange={(value) => { | |||
| const val = value == 0 ? undefined : value; | |||
| setShelfLife(val); | |||
| calculateDates(val, "shelfLife"); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={1} sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| height: '100%', | |||
| }}> | |||
| <SwapHoriz sx={{fontSize: '80px', color:'secondary.main'}}/> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <DatePicker | |||
| sx={textfieldSx} | |||
| label={t("expiryDate")} | |||
| format={OUTPUT_DATE_FORMAT} | |||
| value={expiryDate ? dayjs(expiryDate) : null} | |||
| onChange={(date) => { | |||
| if (!date) { | |||
| setExpiryDate(undefined); | |||
| } else { | |||
| setExpiryDate(date); | |||
| calculateDates(date, "expiryDate"); | |||
| } | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| error: errors.expiryDate.length > 0, | |||
| helperText: errors.expiryDate, | |||
| }, | |||
| }} | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="flex-end" | |||
| spacing={2} | |||
| sx={{ mt: 2 }} | |||
| > | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| sx={{fontSize: '24px'}} | |||
| disabled={expiryDate === undefined || hasError} | |||
| onClick={handleSubmit} | |||
| > | |||
| {t("confirm expiry date")} | |||
| </Button> | |||
| </Stack> | |||
| </LocalizationProvider> | |||
| </Box> | |||
| </Card> | |||
| </Modal> | |||
| ) | |||
| } | |||
| export default CalculateExpiryDateModal; | |||
| @@ -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<Props> = ({ | |||
| 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<Props> = ({ | |||
| 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<Props> = ({ | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| {putawayMode ? ( | |||
| {putawayMode ? (<> | |||
| <Grid item xs={3}> | |||
| <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} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| {...register("acceptedQty", { | |||
| required: "acceptedQty required!", | |||
| })} | |||
| /> | |||
| </Grid></> | |||
| ) : ( | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| @@ -397,13 +408,9 @@ const FgStockInForm: React.FC<Props> = ({ | |||
| {...register("acceptedQty", { | |||
| required: "acceptedQty required!", | |||
| })} | |||
| // disabled={true} | |||
| // disabled={disabled} | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid> | |||
| )} | |||
| {/* <Grid item xs={4}> | |||
| <TextField | |||
| label={t("acceptedWeight")} | |||
| @@ -0,0 +1,153 @@ | |||
| 'use client'; | |||
| import { useState, useEffect, useMemo } from 'react'; | |||
| import { Box, TextField, FormHelperText, styled } from '@mui/material'; | |||
| import { useTranslation } from "react-i18next"; | |||
| interface ShelfLifeInputProps { | |||
| value?: number; // Total days | |||
| onChange?: (value: number) => 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<ShelfLifeInputProps> = ({ 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<HTMLInputElement> | |||
| ) => { | |||
| 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 ( | |||
| <Box sx={{ width: '100%', paddingLeft: '1rem' }}> | |||
| <ShelfLifeContainer> | |||
| <TextField | |||
| label="年" | |||
| value={duration.years} | |||
| onChange={handleChange('years')} | |||
| sx={sx} | |||
| type="text" | |||
| inputProps={{ | |||
| inputMode: 'numeric', | |||
| pattern: '[0-9]*', | |||
| }} | |||
| size="small" | |||
| /> | |||
| <TextField | |||
| label="月" | |||
| value={duration.months} | |||
| onChange={handleChange('months')} | |||
| type="text" | |||
| sx={sx} | |||
| inputProps={{ | |||
| inputMode: 'numeric', | |||
| pattern: '[0-9]*', | |||
| }} | |||
| size="small" | |||
| /> | |||
| <TextField | |||
| label="日" | |||
| value={duration.days} | |||
| onChange={handleChange('days')} | |||
| sx={sx} | |||
| type="text" | |||
| inputProps={{ | |||
| inputMode: 'numeric', | |||
| pattern: '[0-9]*', | |||
| }} | |||
| size="small" | |||
| /> | |||
| </ShelfLifeContainer> | |||
| <FormHelperText sx={{ fontSize: '2rem', mt: 1 }}> | |||
| {label}: <span style={{ color: totalDays < 1 ? 'red':'inherit' }}> | |||
| {/* {formatDuration(duration.years, duration.months, duration.days)} */} | |||
| {totalDays} 日 | |||
| </span> | |||
| </FormHelperText> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ShelfLifeInput; | |||
| @@ -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<Props> = ({ | |||
| const expiryDate = watch("expiryDate"); | |||
| const uom = watch("uom"); | |||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||
| const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(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<Props> = ({ | |||
| // 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 ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| {/* <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| @@ -211,6 +219,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| 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<Props> = ({ | |||
| 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} | |||
| @@ -338,6 +295,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| 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<Props> = ({ | |||
| // field.onChange(date); | |||
| }} | |||
| 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> | |||
| ),}) | |||
| }, | |||
| // required: true, | |||
| error: Boolean(errors.expiryDate?.message), | |||
| helperText: errors.expiryDate?.message, | |||
| onClick: () => setOpenExpDatePicker(true), | |||
| }, | |||
| }} | |||
| /> | |||
| @@ -359,31 +336,45 @@ const StockInForm: React.FC<Props> = ({ | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| {putawayMode ? ( | |||
| {putawayMode || (<> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| label={t("receivedQty")} | |||
| fullWidth | |||
| {...register("receivedQty", { | |||
| required: "receivedQty required!", | |||
| })} | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| value={itemDetail.acceptedQty} | |||
| // disabled={true} | |||
| // disabled={disabled} | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| ) : ( | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("receivedQty")} | |||
| label={t("qty")} | |||
| fullWidth | |||
| {...register("receivedQty", { | |||
| required: "receivedQty required!", | |||
| {...register("qty", { | |||
| required: "qty required!", | |||
| })} | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| /> | |||
| )} | |||
| </Grid> | |||
| </Grid></> | |||
| )} | |||
| {putawayMode && ( | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| value={itemDetail.acceptedQty} | |||
| // disabled={true} | |||
| // disabled={disabled} | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| )} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("uom")} | |||
| @@ -456,6 +447,14 @@ const StockInForm: React.FC<Props> = ({ | |||
| </Grid> */} | |||
| </Grid> | |||
| </Grid> | |||
| <CalculateExpiryDateModal | |||
| open={openModal} | |||
| onClose={handleOnModalClose} | |||
| onSubmit={handleReturnExpiryDate} | |||
| textfieldSx={textfieldSx} | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| export default StockInForm; | |||
| @@ -15,7 +15,7 @@ | |||
| "name": "名稱", | |||
| "description": "描述", | |||
| "Type": "類型", | |||
| "shelfLife": "保存期限", | |||
| "shelfLife": "保質期", | |||
| "remarks": "備註", | |||
| "countryOfOrigin": "原產地", | |||
| "maxQty": "最大數量", | |||
| @@ -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": "無效日期" | |||
| } | |||