| @@ -17,7 +17,7 @@ const PurchaseOrder: React.FC = async () => { | |||||
| // preloadClaims(); | // preloadClaims(); | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <I18nProvider namespaces={["purchaseOrder", "common"]}> | |||||
| <I18nProvider namespaces={["purchaseOrder", "common", "items"]}> | |||||
| <Stack | <Stack | ||||
| direction="row" | direction="row" | ||||
| justifyContent="space-between" | 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 { StockInLine } from "@/app/api/stockIn"; | ||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | 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 } from "@/app/utils/formatUtil"; | |||||
| import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| // change PurchaseQcResult to stock in entry props | // change PurchaseQcResult to stock in entry props | ||||
| interface Props { | interface Props { | ||||
| @@ -60,14 +60,14 @@ const textfieldSx = { | |||||
| height: "100%", | height: "100%", | ||||
| boxSizing: "border-box", | boxSizing: "border-box", | ||||
| padding: "0.75rem", | padding: "0.75rem", | ||||
| fontSize: 24, | |||||
| fontSize: 30, | |||||
| }, | }, | ||||
| "& .MuiInputLabel-root": { | "& .MuiInputLabel-root": { | ||||
| fontSize: 24, | |||||
| transform: "translate(14px, 1.5rem) scale(1)", | |||||
| fontSize: 30, | |||||
| transform: "translate(14px, 1.2rem) scale(1)", | |||||
| "&.MuiInputLabel-shrink": { | "&.MuiInputLabel-shrink": { | ||||
| fontSize: 18, | |||||
| transform: "translate(14px, -9px) scale(1)", | |||||
| fontSize: 24, | |||||
| transform: "translate(14px, -0.5rem) scale(1)", | |||||
| }, | }, | ||||
| // [theme.breakpoints.down("sm")]: { | // [theme.breakpoints.down("sm")]: { | ||||
| // fontSize: "1rem", | // fontSize: "1rem", | ||||
| @@ -200,6 +200,7 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| label={t("receiptDate")} | label={t("receiptDate")} | ||||
| value={dayjs(watch("receiptDate"))} | value={dayjs(watch("receiptDate"))} | ||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={true} | disabled={true} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| @@ -317,6 +318,7 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| label={t("expiryDate")} | label={t("expiryDate")} | ||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | value={expiryDate ? dayjs(expiryDate) : undefined} | ||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={disabled} | disabled={disabled} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| @@ -375,20 +377,29 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| disabled={true} | disabled={true} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| {putawayMode ? ( | |||||
| {putawayMode ? (<> | |||||
| <Grid item xs={3}> | |||||
| <TextField | <TextField | ||||
| label={t("processedQty")} | label={t("processedQty")} | ||||
| fullWidth | fullWidth | ||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| disabled={true} | disabled={true} | ||||
| value={itemDetail.putAwayLines?.reduce((sum, p) => sum + p.qty, 0) ?? 0} | 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 | <TextField | ||||
| label={t("acceptedQty")} | label={t("acceptedQty")} | ||||
| fullWidth | fullWidth | ||||
| @@ -397,13 +408,9 @@ const FgStockInForm: React.FC<Props> = ({ | |||||
| {...register("acceptedQty", { | {...register("acceptedQty", { | ||||
| required: "acceptedQty required!", | required: "acceptedQty required!", | ||||
| })} | })} | ||||
| // disabled={true} | |||||
| // disabled={disabled} | |||||
| // error={Boolean(errors.acceptedQty)} | |||||
| // helperText={errors.acceptedQty?.message} | |||||
| /> | /> | ||||
| )} | |||||
| </Grid> | |||||
| </Grid> | |||||
| )} | |||||
| {/* <Grid item xs={4}> | {/* <Grid item xs={4}> | ||||
| <TextField | <TextField | ||||
| label={t("acceptedWeight")} | 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"; | } from "@/app/api/stockIn/actions"; | ||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | |||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| Grid, | Grid, | ||||
| InputAdornment, | |||||
| Stack, | Stack, | ||||
| TextField, | TextField, | ||||
| Tooltip, | Tooltip, | ||||
| @@ -16,25 +18,16 @@ import { | |||||
| 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, useState, useMemo } from "react"; | |||||
| import { | import { | ||||
| GridColDef, | |||||
| GridRowIdGetter, | |||||
| GridRowModel, | |||||
| useGridApiContext, | |||||
| GridRenderCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiRef, | useGridApiRef, | ||||
| } from "@mui/x-data-grid"; | } 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 { StockInLine } from "@/app/api/stockIn"; | ||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | 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 } from "@/app/utils/formatUtil"; | |||||
| import { dayjsToDateString, INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import CalculateExpiryDateModal from "./CalculateExpiryDateModal"; | |||||
| // change PurchaseQcResult to stock in entry props | // change PurchaseQcResult to stock in entry props | ||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | itemDetail: StockInLine; | ||||
| @@ -60,14 +53,14 @@ const textfieldSx = { | |||||
| height: "100%", | height: "100%", | ||||
| boxSizing: "border-box", | boxSizing: "border-box", | ||||
| padding: "0.75rem", | padding: "0.75rem", | ||||
| fontSize: 24, | |||||
| fontSize: 30, | |||||
| }, | }, | ||||
| "& .MuiInputLabel-root": { | "& .MuiInputLabel-root": { | ||||
| fontSize: 24, | |||||
| transform: "translate(14px, 1.5rem) scale(1)", | |||||
| fontSize: 30, | |||||
| transform: "translate(14px, 1.2rem) scale(1)", | |||||
| "&.MuiInputLabel-shrink": { | "&.MuiInputLabel-shrink": { | ||||
| fontSize: 18, | |||||
| transform: "translate(14px, -9px) scale(1)", | |||||
| fontSize: 24, | |||||
| transform: "translate(14px, -0.5rem) scale(1)", | |||||
| }, | }, | ||||
| // [theme.breakpoints.down("sm")]: { | // [theme.breakpoints.down("sm")]: { | ||||
| // fontSize: "1rem", | // fontSize: "1rem", | ||||
| @@ -119,11 +112,11 @@ const StockInForm: React.FC<Props> = ({ | |||||
| const expiryDate = watch("expiryDate"); | const expiryDate = watch("expiryDate"); | ||||
| const uom = watch("uom"); | const uom = watch("uom"); | ||||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||||
| const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false); | |||||
| //// TODO : Add Checking //// | //// TODO : Add Checking //// | ||||
| // Check if dates are input | // Check if dates are input | ||||
| // if (data.productionDate === undefined || data.productionDate == null) { | |||||
| // validationErrors.push("請輸入生產日期!"); | |||||
| // } | |||||
| // if (data.expiryDate === undefined || data.expiryDate == null) { | // if (data.expiryDate === undefined || data.expiryDate == null) { | ||||
| // validationErrors.push("請輸入到期日!"); | // validationErrors.push("請輸入到期日!"); | ||||
| // } | // } | ||||
| @@ -133,14 +126,29 @@ const StockInForm: React.FC<Props> = ({ | |||||
| // console.log(productionDate); | // console.log(productionDate); | ||||
| // console.log(expiryDate); | // console.log(expiryDate); | ||||
| if (expiryDate) clearErrors(); | if (expiryDate) clearErrors(); | ||||
| if (productionDate) clearErrors(); | |||||
| }, [productionDate, expiryDate, clearErrors]); | }, [productionDate, expiryDate, clearErrors]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail); | ||||
| }, [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 ( | 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}> | ||||
| @@ -211,6 +219,7 @@ const StockInForm: React.FC<Props> = ({ | |||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| label={t("receiptDate")} | label={t("receiptDate")} | ||||
| value={dayjs(watch("receiptDate"))} | value={dayjs(watch("receiptDate"))} | ||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={true} | disabled={true} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| @@ -270,58 +279,6 @@ const StockInForm: React.FC<Props> = ({ | |||||
| helperText={errors.productLotNo?.message} | helperText={errors.productLotNo?.message} | ||||
| />)} | />)} | ||||
| </Grid> | </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}> | <Grid item xs={6}> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| @@ -338,6 +295,7 @@ const StockInForm: React.FC<Props> = ({ | |||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| label={t("expiryDate")} | label={t("expiryDate")} | ||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | value={expiryDate ? dayjs(expiryDate) : undefined} | ||||
| format={OUTPUT_DATE_FORMAT} | |||||
| disabled={disabled} | disabled={disabled} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| @@ -346,11 +304,30 @@ const StockInForm: React.FC<Props> = ({ | |||||
| // field.onChange(date); | // field.onChange(date); | ||||
| }} | }} | ||||
| inputRef={field.ref} | inputRef={field.ref} | ||||
| open={openExpDatePicker && !openModal} | |||||
| onOpen={() => setOpenExpDatePicker(true)} | |||||
| onClose={() => setOpenExpDatePicker(false)} | |||||
| slotProps={{ | slotProps={{ | ||||
| textField: { | 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, | // required: true, | ||||
| error: Boolean(errors.expiryDate?.message), | error: Boolean(errors.expiryDate?.message), | ||||
| helperText: errors.expiryDate?.message, | helperText: errors.expiryDate?.message, | ||||
| onClick: () => setOpenExpDatePicker(true), | |||||
| }, | }, | ||||
| }} | }} | ||||
| /> | /> | ||||
| @@ -359,31 +336,45 @@ const StockInForm: React.FC<Props> = ({ | |||||
| }} | }} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| {putawayMode ? ( | |||||
| {putawayMode || (<> | |||||
| <Grid item xs={3}> | |||||
| <TextField | <TextField | ||||
| label={t("acceptedQty")} | |||||
| label={t("receivedQty")} | |||||
| fullWidth | fullWidth | ||||
| {...register("receivedQty", { | |||||
| required: "receivedQty required!", | |||||
| })} | |||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| disabled={true} | disabled={true} | ||||
| value={itemDetail.acceptedQty} | |||||
| // disabled={true} | |||||
| // disabled={disabled} | |||||
| // error={Boolean(errors.acceptedQty)} | |||||
| // helperText={errors.acceptedQty?.message} | |||||
| /> | /> | ||||
| ) : ( | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <TextField | <TextField | ||||
| label={t("receivedQty")} | |||||
| label={t("qty")} | |||||
| fullWidth | fullWidth | ||||
| {...register("receivedQty", { | |||||
| required: "receivedQty required!", | |||||
| {...register("qty", { | |||||
| required: "qty required!", | |||||
| })} | })} | ||||
| sx={textfieldSx} | sx={textfieldSx} | ||||
| disabled={true} | 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}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("uom")} | label={t("uom")} | ||||
| @@ -456,6 +447,14 @@ const StockInForm: React.FC<Props> = ({ | |||||
| </Grid> */} | </Grid> */} | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| <CalculateExpiryDateModal | |||||
| open={openModal} | |||||
| onClose={handleOnModalClose} | |||||
| onSubmit={handleReturnExpiryDate} | |||||
| textfieldSx={textfieldSx} | |||||
| /> | |||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default StockInForm; | export default StockInForm; | ||||
| @@ -15,7 +15,7 @@ | |||||
| "name": "名稱", | "name": "名稱", | ||||
| "description": "描述", | "description": "描述", | ||||
| "Type": "類型", | "Type": "類型", | ||||
| "shelfLife": "保存期限", | |||||
| "shelfLife": "保質期", | |||||
| "remarks": "備註", | "remarks": "備註", | ||||
| "countryOfOrigin": "原產地", | "countryOfOrigin": "原產地", | ||||
| "maxQty": "最大數量", | "maxQty": "最大數量", | ||||
| @@ -157,5 +157,12 @@ | |||||
| "salesUnit": "銷售單位", | "salesUnit": "銷售單位", | ||||
| "download Qr Code": "下載QR碼", | "download Qr Code": "下載QR碼", | ||||
| "downloading": "下載中", | "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": "無效日期" | |||||
| } | } | ||||