Browse Source

expiry modal

production
tommy 1 month ago
parent
commit
c90cee778f
2 changed files with 319 additions and 111 deletions
  1. +187
    -60
      src/components/StockIn/CalculateExpiryDateModal.tsx
  2. +132
    -51
      src/components/StockIn/ShelfLifeInput.tsx

+ 187
- 60
src/components/StockIn/CalculateExpiryDateModal.tsx View File

@@ -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>


+ 132
- 51
src/components/StockIn/ShelfLifeInput.tsx View File

@@ -1,7 +1,7 @@
'use client';

import { useState, useEffect, useMemo } from 'react';
import { Box, TextField, FormHelperText, styled } from '@mui/material';
import { Box, TextField, FormHelperText, Typography, styled } from '@mui/material';
import { useTranslation } from "react-i18next";

interface ShelfLifeInputProps {
@@ -9,16 +9,39 @@ interface ShelfLifeInputProps {
onChange?: (value: number) => void;
label?: string;
sx?: any;
showHelperText?: boolean; // Option to show/hide the helper text
showHelperText?: boolean;
/** When true, shows "保質期: X 日" under the 年 field only */
helperUnderYear?: boolean;
/** Labels above fields instead of on the outline (avoids clipping) */
externalLabels?: boolean;
/** Only render Y/M/D inputs (labels & helper rendered by parent) */
inputsOnly?: boolean;
}

const UnitLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Typography
component="span"
sx={{
fontWeight: 600,
fontSize: { xs: '0.95rem', sm: '1.05rem' },
lineHeight: 1.4,
color: 'text.secondary',
mb: 0.75,
textAlign: 'center',
}}
>
{children}
</Typography>
);

const ShelfLifeContainer = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
gap: theme.spacing(2),
alignItems: 'flex-start',
width: '100%',
'& .MuiTextField-root': {
flex: 1,
minWidth: 88,
'& input': {
textAlign: 'center',
},
@@ -62,7 +85,16 @@ const formatDuration = (years: number, months: number, days: number) => {
return parts.length > 0 ? parts.join(' ') : '0 日';
};

const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = () => {}, label = 'Shelf Life', sx, showHelperText = true }) => {
const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({
value = 0,
onChange = () => {},
label = 'Shelf Life',
sx,
showHelperText = true,
helperUnderYear = false,
externalLabels = false,
inputsOnly = false,
}) => {
const { t } = useTranslation("purchaseOrder");
const { years, months, days } = daysToDuration(value);
@@ -101,54 +133,103 @@ const ShelfLifeInput: React.FC<ShelfLifeInputProps> = ({ value = 0, onChange = (
}
};

const shelfLifeSummary = showHelperText && (
<Typography
component="div"
sx={{
fontSize: { xs: '0.95rem', sm: '1.05rem' },
mt: 0.75,
lineHeight: 1.4,
whiteSpace: 'nowrap',
color: 'text.secondary',
}}
>
{label}:{' '}
<Box component="span" sx={{ color: 'error.main', fontWeight: 600 }}>
{totalDays} 日
</Box>
</Typography>
);

const hiddenLabelProps = { shrink: true, sx: { display: 'none' } };

const yearField = (
<TextField
label={externalLabels ? ' ' : '年'}
value={duration.years}
onChange={handleChange('years')}
sx={sx}
type="text"
fullWidth
InputLabelProps={externalLabels ? hiddenLabelProps : { shrink: true }}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
);

const monthField = (
<TextField
label={externalLabels ? ' ' : '月'}
value={duration.months}
onChange={handleChange('months')}
type="text"
sx={sx}
fullWidth
InputLabelProps={externalLabels ? hiddenLabelProps : { shrink: true }}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
);

const dayField = (
<TextField
label={externalLabels ? ' ' : '日'}
value={duration.days}
onChange={handleChange('days')}
sx={sx}
type="text"
fullWidth
InputLabelProps={externalLabels ? hiddenLabelProps : { shrink: true }}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
);

const inputsRow = (
<ShelfLifeContainer>
<Box sx={{ flex: 1, minWidth: 0, overflow: 'visible' }}>
{!inputsOnly && externalLabels && <UnitLabel>年</UnitLabel>}
{yearField}
{!inputsOnly && helperUnderYear && shelfLifeSummary}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
{!inputsOnly && externalLabels && <UnitLabel>月</UnitLabel>}
{monthField}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
{!inputsOnly && externalLabels && <UnitLabel>日</UnitLabel>}
{dayField}
</Box>
</ShelfLifeContainer>
);

if (inputsOnly) {
return <Box sx={{ width: '100%' }}>{inputsRow}</Box>;
}

return (
<Box sx={{ width: '100%' }}>
<ShelfLifeContainer>
<TextField
label="年"
value={duration.years}
onChange={handleChange('years')}
sx={sx}
type="text"
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
<TextField
label="月"
value={duration.months}
onChange={handleChange('months')}
type="text"
sx={sx}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
<TextField
label="日"
value={duration.days}
onChange={handleChange('days')}
sx={sx}
type="text"
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
size="medium"
/>
</ShelfLifeContainer>
{showHelperText && (
<FormHelperText sx={{ fontSize: '0.875rem', mt: 1 }}>
{label}: <span style={{ color: totalDays < 1 ? 'red':'inherit' }}>
{/* {formatDuration(duration.years, duration.months, duration.days)} */}
{totalDays} 日
</span>
</FormHelperText>
)}
<Box sx={{ width: '100%', overflow: 'visible' }}>
{inputsRow}
{showHelperText && !helperUnderYear && shelfLifeSummary}
</Box>
);
};


Loading…
Cancel
Save