|
|
|
@@ -1,7 +1,7 @@ |
|
|
|
|
|
|
|
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 { Box, Button, Card, FormHelperText, Modal, Stack, 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"; |
|
|
|
@@ -24,6 +24,78 @@ type EntryError = |
|
|
|
shelfLife: string, |
|
|
|
} |
|
|
|
|
|
|
|
const INPUT_HEIGHT = "5rem"; |
|
|
|
const ICON_COL_WIDTH = 48; |
|
|
|
|
|
|
|
const iconColumnSx = { |
|
|
|
width: ICON_COL_WIDTH, |
|
|
|
minWidth: ICON_COL_WIDTH, |
|
|
|
height: INPUT_HEIGHT, |
|
|
|
display: "flex", |
|
|
|
alignItems: "center", |
|
|
|
justifyContent: "center", |
|
|
|
flexShrink: 0, |
|
|
|
}; |
|
|
|
|
|
|
|
const columnSx = { |
|
|
|
flex: { sm: "1 1 22%" }, |
|
|
|
minWidth: { sm: 180 }, |
|
|
|
width: { xs: "100%", sm: "auto" }, |
|
|
|
}; |
|
|
|
|
|
|
|
const shelfColumnSx = { |
|
|
|
flex: { sm: "1 1 28%" }, |
|
|
|
minWidth: { sm: 200 }, |
|
|
|
width: { xs: "100%", sm: "auto" }, |
|
|
|
}; |
|
|
|
|
|
|
|
const rowSx = { |
|
|
|
display: "flex", |
|
|
|
flexDirection: { xs: "column", sm: "row" }, |
|
|
|
gap: { xs: 2, sm: 2 }, |
|
|
|
overflowX: { sm: "auto" }, |
|
|
|
}; |
|
|
|
|
|
|
|
const defaultModalFieldSx = { |
|
|
|
width: "100%", |
|
|
|
"& .MuiInputBase-root": { |
|
|
|
height: INPUT_HEIGHT, |
|
|
|
}, |
|
|
|
"& .MuiInputBase-input": { |
|
|
|
height: "100%", |
|
|
|
boxSizing: "border-box", |
|
|
|
padding: "0.75rem 2.75rem 0.75rem 0.75rem", |
|
|
|
fontSize: { xs: 22, sm: 26 }, |
|
|
|
"&::placeholder": { |
|
|
|
fontSize: { xs: 20, sm: 24 }, |
|
|
|
opacity: 0.55, |
|
|
|
}, |
|
|
|
}, |
|
|
|
"& .MuiInputLabel-root": { |
|
|
|
display: "none", |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
const hiddenPickerLabelProps = { |
|
|
|
shrink: true, |
|
|
|
sx: { display: "none" }, |
|
|
|
}; |
|
|
|
|
|
|
|
const FieldLabel: React.FC<{ children: React.ReactNode; sx?: object }> = ({ children, sx }) => ( |
|
|
|
<Typography |
|
|
|
component="div" |
|
|
|
sx={{ |
|
|
|
fontWeight: 600, |
|
|
|
fontSize: { xs: "0.95rem", sm: "1.05rem" }, |
|
|
|
lineHeight: 1.4, |
|
|
|
color: "text.secondary", |
|
|
|
...sx, |
|
|
|
}} |
|
|
|
> |
|
|
|
{children} |
|
|
|
</Typography> |
|
|
|
); |
|
|
|
|
|
|
|
const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
open, |
|
|
|
onClose, |
|
|
|
@@ -31,6 +103,14 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
textfieldSx, |
|
|
|
}) => { |
|
|
|
const { t, i18n: { language }, } = useTranslation("purchaseOrder"); |
|
|
|
const fieldSx = { |
|
|
|
...(textfieldSx ?? defaultModalFieldSx), |
|
|
|
"& .MuiInputLabel-root": { display: "none" }, |
|
|
|
"& .MuiInputBase-input::placeholder": { |
|
|
|
fontSize: { xs: 20, sm: 24 }, |
|
|
|
opacity: 0.55, |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
const [productionDate, setProductionDate] = useState<Dayjs>(); |
|
|
|
const [shelfLife, setShelfLife] = useState<number>(); |
|
|
|
@@ -158,24 +238,27 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
onClose={onModalClose} |
|
|
|
> |
|
|
|
<Card |
|
|
|
style={{ |
|
|
|
flex: 10, |
|
|
|
marginBottom: "20px", |
|
|
|
width: "70%", |
|
|
|
// height: "80%", |
|
|
|
sx={{ |
|
|
|
position: "fixed", |
|
|
|
top: "50%", |
|
|
|
left: "50%", |
|
|
|
transform: "translate(-50%, -50%) scale(0.8)", |
|
|
|
transform: "translate(-50%, -50%)", |
|
|
|
width: { xs: "96%", sm: "94%", md: "92%" }, |
|
|
|
minWidth: { sm: 640, md: 820 }, |
|
|
|
maxWidth: 1100, |
|
|
|
maxHeight: "90vh", |
|
|
|
overflowY: "auto", |
|
|
|
overflowX: "visible", |
|
|
|
}} |
|
|
|
> |
|
|
|
<Box |
|
|
|
sx={{ |
|
|
|
display: "block", |
|
|
|
"flex-direction": "column", |
|
|
|
padding: "20px", |
|
|
|
height: "100%", //'30rem', |
|
|
|
padding: { xs: 3, sm: 4 }, |
|
|
|
height: "100%", |
|
|
|
width: "100%", |
|
|
|
overflow: "visible", |
|
|
|
"& .actions": { |
|
|
|
color: "text.secondary", |
|
|
|
}, |
|
|
|
@@ -190,22 +273,41 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
dateAdapter={AdapterDayjs} |
|
|
|
adapterLocale={`${language}-hk`} |
|
|
|
> |
|
|
|
<Stack sx={{mb: 1}}> |
|
|
|
<Typography variant="h6" component="h2" sx={{ fontWeight: 'bold', mb: 2 }}> |
|
|
|
<Stack sx={{ mb: 1 }}> |
|
|
|
<Typography |
|
|
|
variant="h6" |
|
|
|
component="h2" |
|
|
|
sx={{ fontWeight: "bold", mb: 2, fontSize: { xs: "1.25rem", sm: "1.5rem" } }} |
|
|
|
> |
|
|
|
{t("Fill in Expiry Date")} |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
<Stack> |
|
|
|
<Grid |
|
|
|
container |
|
|
|
justifyContent="flex-start" |
|
|
|
alignItems="flex-start" |
|
|
|
spacing={2} |
|
|
|
> |
|
|
|
<Grid item xs={4}> |
|
|
|
<Box sx={{ overflow: "visible" }}> |
|
|
|
{/* Label row */} |
|
|
|
<Box sx={{ ...rowSx, mb: 0.75 }}> |
|
|
|
<Box sx={columnSx}> |
|
|
|
<FieldLabel>{t("productionDate")}</FieldLabel> |
|
|
|
</Box> |
|
|
|
<Box sx={{ width: ICON_COL_WIDTH, flexShrink: 0, display: { xs: "none", sm: "block" } }} aria-hidden /> |
|
|
|
<Box sx={shelfColumnSx}> |
|
|
|
<Box sx={{ display: "flex", gap: 2 }}> |
|
|
|
<FieldLabel sx={{ flex: 1, textAlign: "center" }}>年</FieldLabel> |
|
|
|
<FieldLabel sx={{ flex: 1, textAlign: "center" }}>月</FieldLabel> |
|
|
|
<FieldLabel sx={{ flex: 1, textAlign: "center" }}>日</FieldLabel> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
<Box sx={{ width: ICON_COL_WIDTH, flexShrink: 0, display: { xs: "none", sm: "block" } }} /> |
|
|
|
<Box sx={columnSx}> |
|
|
|
<FieldLabel>{t("expiryDate")}</FieldLabel> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
|
|
|
|
{/* Input row — icons align with fields only */} |
|
|
|
<Box sx={{ ...rowSx, alignItems: { xs: "stretch", sm: "center" } }}> |
|
|
|
<Box sx={columnSx}> |
|
|
|
<DatePicker |
|
|
|
sx={textfieldSx} |
|
|
|
label={t("productionDate")} |
|
|
|
sx={fieldSx} |
|
|
|
label=" " |
|
|
|
value={productionDate ? dayjs(productionDate) : null} |
|
|
|
format={OUTPUT_DATE_FORMAT} |
|
|
|
onChange={(date) => { |
|
|
|
@@ -218,51 +320,38 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
}} |
|
|
|
slotProps={{ |
|
|
|
textField: { |
|
|
|
error: errors.productionDate.length > 0, |
|
|
|
helperText: errors.productionDate, |
|
|
|
fullWidth: true, |
|
|
|
InputLabelProps: hiddenPickerLabelProps, |
|
|
|
error: errors.productionDate.length > 0, |
|
|
|
}, |
|
|
|
}} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={0.5} sx={{ |
|
|
|
display: 'flex', |
|
|
|
justifyContent: 'center', |
|
|
|
alignItems: 'center', |
|
|
|
alignSelf: 'flex-start', |
|
|
|
mt: '20px', // align icon with vertical center of 生產日期 input (label ~20px + half input 28px) |
|
|
|
height: 56, |
|
|
|
}}> |
|
|
|
<Add sx={{ |
|
|
|
fontSize: '1.25rem', color:'secondary.main', |
|
|
|
}}/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={2.5}> |
|
|
|
</Box> |
|
|
|
<Box sx={iconColumnSx}> |
|
|
|
<Add sx={{ fontSize: "2rem", color: "secondary.main" }} /> |
|
|
|
</Box> |
|
|
|
<Box sx={shelfColumnSx}> |
|
|
|
<ShelfLifeInput |
|
|
|
sx={textfieldSx} |
|
|
|
sx={fieldSx} |
|
|
|
value={shelfLife} |
|
|
|
label={t("shelfLife")} |
|
|
|
showHelperText={false} |
|
|
|
externalLabels |
|
|
|
inputsOnly |
|
|
|
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', |
|
|
|
alignSelf: 'flex-start', |
|
|
|
mt: '20px', // align with 生產日期 input text box |
|
|
|
height: 56, |
|
|
|
}}> |
|
|
|
<SwapHoriz sx={{fontSize: '1.25rem', color:'secondary.main'}}/> |
|
|
|
</Grid> |
|
|
|
<Grid item xs={4}> |
|
|
|
</Box> |
|
|
|
<Box sx={iconColumnSx}> |
|
|
|
<SwapHoriz sx={{ fontSize: "2rem", color: "secondary.main" }} /> |
|
|
|
</Box> |
|
|
|
<Box sx={{ ...columnSx, minWidth: { sm: 200 } }}> |
|
|
|
<DatePicker |
|
|
|
sx={textfieldSx} |
|
|
|
label={t("expiryDate")} |
|
|
|
sx={fieldSx} |
|
|
|
label=" " |
|
|
|
format={OUTPUT_DATE_FORMAT} |
|
|
|
value={expiryDate ? dayjs(expiryDate) : null} |
|
|
|
onChange={(date) => { |
|
|
|
@@ -275,15 +364,48 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
}} |
|
|
|
slotProps={{ |
|
|
|
textField: { |
|
|
|
error: errors.expiryDate.length > 0, |
|
|
|
helperText: errors.expiryDate, |
|
|
|
fullWidth: true, |
|
|
|
InputLabelProps: hiddenPickerLabelProps, |
|
|
|
error: errors.expiryDate.length > 0, |
|
|
|
}, |
|
|
|
}} |
|
|
|
disabled={true} |
|
|
|
disabled |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
</Stack> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
|
|
|
|
{/* Helper / error row */} |
|
|
|
<Box sx={{ ...rowSx, mt: 0.75, alignItems: "flex-start" }}> |
|
|
|
<Box sx={columnSx}> |
|
|
|
{errors.productionDate && ( |
|
|
|
<FormHelperText error>{errors.productionDate}</FormHelperText> |
|
|
|
)} |
|
|
|
</Box> |
|
|
|
<Box sx={{ width: ICON_COL_WIDTH, flexShrink: 0, display: { xs: "none", sm: "block" } }} /> |
|
|
|
<Box sx={shelfColumnSx}> |
|
|
|
<Typography |
|
|
|
component="div" |
|
|
|
sx={{ |
|
|
|
fontSize: { xs: "0.95rem", sm: "1.05rem" }, |
|
|
|
whiteSpace: "nowrap", |
|
|
|
color: "text.secondary", |
|
|
|
width: { sm: "33%" }, |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("shelfLife")}:{" "} |
|
|
|
<Box component="span" sx={{ color: "error.main", fontWeight: 600 }}> |
|
|
|
{shelfLife ?? 0} 日 |
|
|
|
</Box> |
|
|
|
</Typography> |
|
|
|
</Box> |
|
|
|
<Box sx={{ width: ICON_COL_WIDTH, flexShrink: 0, display: { xs: "none", sm: "block" } }} /> |
|
|
|
<Box sx={columnSx}> |
|
|
|
{errors.expiryDate && ( |
|
|
|
<FormHelperText error>{errors.expiryDate}</FormHelperText> |
|
|
|
)} |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
<Stack |
|
|
|
direction="row" |
|
|
|
justifyContent="flex-end" |
|
|
|
@@ -293,9 +415,14 @@ const CalculateExpiryDateModal: React.FC<Props> = ({ |
|
|
|
<Button |
|
|
|
name="submit" |
|
|
|
variant="contained" |
|
|
|
startIcon={<Check />} |
|
|
|
startIcon={<Check sx={{ fontSize: "1.75rem" }} />} |
|
|
|
disabled={expiryDate === undefined || hasError} |
|
|
|
onClick={handleSubmit} |
|
|
|
sx={{ |
|
|
|
minHeight: "3.5rem", |
|
|
|
px: 3, |
|
|
|
fontSize: { xs: "1rem", sm: "1.25rem" }, |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("confirm expiry date")} |
|
|
|
</Button> |
|
|
|
|