| @@ -28,7 +28,7 @@ import { fetchPickOrderClient, autoAssignAndReleasePickOrder } from "@/app/api/p | |||
| import Jobcreatitem from "./Jobcreatitem"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import PickExecutionDetail from "./GoodPickExecutiondetail"; | |||
| interface Props { | |||
| pickOrders: PickOrderResult[]; | |||
| } | |||
| @@ -276,27 +276,22 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| p: 2, | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Stack rowGap={2}> | |||
| <Grid container> | |||
| <Stack rowGap={2}> | |||
| <Grid container alignItems="center"> | |||
| <Grid item xs={8}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Finished Good Order")} | |||
| </Typography> | |||
| </Grid> | |||
| {/* | |||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | |||
| <Button onClick={openCreateModal}> | |||
| {t("create")} | |||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="center"> | |||
| <Button | |||
| variant="contained" | |||
| onClick={handleManualAssign} | |||
| disabled={isAssigning} | |||
| > | |||
| {isAssigning ? t("Assigning pick order...") : t("Assign & Release")} | |||
| </Button> | |||
| {isOpenCreateModal && | |||
| <CreatePickOrderModal | |||
| open={isOpenCreateModal} | |||
| onClose={closeCreateModal} | |||
| items={items} | |||
| /> | |||
| } | |||
| </Grid> | |||
| */} | |||
| </Grid> | |||
| </Stack> | |||
| </Box> | |||
| @@ -306,26 +301,10 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab | |||
| label={t("Pick Execution")} | |||
| iconPosition="end" | |||
| onClick={handleManualAssign} | |||
| sx={{ | |||
| cursor: 'pointer', | |||
| '&:hover': { | |||
| color: 'primary.main', | |||
| } | |||
| }} | |||
| title={t("Click to assign a new pick order")} | |||
| /> | |||
| <Tab label={t("Pick Execution")} iconPosition="end" /> | |||
| <Tab label={t("Pick Execution Detail")} iconPosition="end" /> | |||
| </Tabs> | |||
| {isAssigning && ( | |||
| <Box sx={{ p: 1, textAlign: 'center' }}> | |||
| <Typography variant="caption" color="primary"> | |||
| {t("Assigning pick order...")} | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| {/* Content section - NO overflow: 'auto' here */} | |||
| @@ -333,6 +312,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| p: 2 | |||
| }}> | |||
| {tabIndex === 0 && <PickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| @@ -934,17 +934,10 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2}> | |||
| {/* Search Box */} | |||
| <Box> | |||
| {/* | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Typography variant="h6" gutterBottom sx={{ mb: 0 }}> | |||
| {t("FG Pick Orders")} | |||
| </Typography> | |||
| </Box> | |||
| */} | |||
| {fgPickOrdersLoading ? ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||
| <CircularProgress /> | |||
| @@ -972,9 +965,8 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| {/* | |||
| {/* Combined Lot Table */} | |||
| <Box> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Typography variant="h6" gutterBottom sx={{ mb: 0 }}> | |||
| @@ -992,12 +984,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot#")}</TableCell> | |||
| <TableCell>{t("Target Date")}</TableCell> | |||
| {/* <TableCell>{t("Lot Location")}</TableCell> */} | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| <TableCell align="right">{t("Original Available Qty")}</TableCell> | |||
| <TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell> | |||
| {/* <TableCell align="right">{t("Remaining Available Qty")}</TableCell> */} | |||
| <TableCell align="center">{t("Action")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -1045,9 +1036,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| <TableCell>{lot.pickOrderTargetDate}</TableCell> | |||
| {/* <TableCell>{lot.location}</TableCell> */} | |||
| <TableCell align="right">{calculateRemainingRequiredQty(lot).toLocaleString()}</TableCell> | |||
| <TableCell align="right"> | |||
| {(() => { | |||
| const inQty = lot.inQty || 0; | |||
| @@ -1057,7 +1046,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| })()} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {/* ✅ QR Scan Button if not scanned, otherwise show TextField + Issue button */} | |||
| {!lot.stockOutLineId ? ( | |||
| <Button | |||
| variant="outlined" | |||
| @@ -1106,12 +1095,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| max: calculateRemainingRequiredQty(lot), | |||
| step: 0.01 | |||
| }} | |||
| sx={{ | |||
| width: '80px', | |||
| sx={{ | |||
| width: '60px', | |||
| height: '28px', | |||
| '& .MuiInputBase-input': { | |||
| fontSize: '0.75rem', | |||
| fontSize: '0.7rem', | |||
| textAlign: 'center', | |||
| padding: '8px 4px' | |||
| padding: '6px 8px' | |||
| } | |||
| }} | |||
| placeholder="0" | |||
| @@ -1136,14 +1126,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| </Stack> | |||
| )} | |||
| </TableCell> | |||
| {/* <TableCell align="right"> | |||
| {(() => { | |||
| const inQty = lot.inQty || 0; | |||
| const outQty = lot.outQty || 0; | |||
| const result = inQty - outQty; | |||
| return result.toLocaleString(); | |||
| })()} | |||
| </TableCell> */} | |||
| <TableCell align="center"> | |||
| <Stack direction="column" spacing={1} alignItems="center"> | |||
| <Button | |||
| @@ -1168,14 +1151,17 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| {t("Submit")} | |||
| </Button> | |||
| </Stack> | |||
| </TableCell> | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| */} | |||
| {/* | |||
| <TablePagination | |||
| component="div" | |||
| count={combinedLotData.length} | |||
| @@ -1192,7 +1178,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| </Box> | |||
| </Stack> | |||
| {/* ✅ QR Code Modal */} | |||
| <QrCodeModal | |||
| open={qrModalOpen} | |||
| onClose={() => { | |||
| @@ -1206,7 +1192,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | |||
| /> | |||
| {/* ✅ Good Pick Execution Form Modal */} | |||
| {pickExecutionFormOpen && selectedLotForExecutionForm && ( | |||
| <GoodPickExecutionForm | |||
| open={pickExecutionFormOpen} | |||
| @@ -1234,6 +1220,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| pickOrderCreateDate={new Date()} | |||
| /> | |||
| )} | |||
| */} | |||
| </FormProvider> | |||
| ); | |||
| }; | |||
| @@ -250,34 +250,34 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('requiredQty')} | |||
| label={t('Required Qty')} | |||
| value={requiredQty || 0} | |||
| disabled | |||
| variant="outlined" | |||
| helperText={t('Still need to pick')} | |||
| // helperText={t('Still need to pick')} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('remainingAvailableQty')} | |||
| label={t('Remaining Available Qty')} | |||
| value={remainingAvailableQty} | |||
| disabled | |||
| variant="outlined" | |||
| helperText={t('Available in warehouse')} | |||
| // helperText={t('Available in warehouse')} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('actualPickQty')} | |||
| label={t('Actual Pick Qty')} | |||
| type="number" | |||
| value={formData.actualPickQty || 0} | |||
| onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} | |||
| error={!!errors.actualPickQty} | |||
| helperText={errors.actualPickQty || t('Enter the quantity actually picked')} | |||
| // helperText={errors.actualPickQty || t('Enter the quantity actually picked')} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -285,12 +285,12 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('missQty')} | |||
| label={t('Missing item Qty')} | |||
| type="number" | |||
| value={formData.missQty || 0} | |||
| onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)} | |||
| error={!!errors.missQty} | |||
| helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} | |||
| // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -298,31 +298,31 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('badItemQty')} | |||
| label={t('Bad Item Qty')} | |||
| type="number" | |||
| value={formData.badItemQty || 0} | |||
| onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} | |||
| error={!!errors.badItemQty} | |||
| helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} | |||
| // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) && ( | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| id="issueRemark" | |||
| label={t('issueRemark')} | |||
| label={t('Issue Remark')} | |||
| multiline | |||
| rows={4} | |||
| value={formData.issueRemark || ''} | |||
| onChange={(e) => handleInputChange('issueRemark', e.target.value)} | |||
| error={!!errors.issueRemark} | |||
| helperText={errors.issueRemark} | |||
| placeholder={t('Describe the issue with bad items')} | |||
| //placeholder={t('Describe the issue with bad items')} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -349,7 +349,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| </FormControl> | |||
| </Grid> | |||
| </> | |||
| )} | |||
| ) : (<></>)} | |||
| </Grid> | |||
| </Box> | |||
| </DialogContent> | |||
| @@ -718,56 +718,56 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| {/* ✅ 恢复 TextField 用于正常数量输入 */} | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''} | |||
| onChange={(e) => { | |||
| if (selectedRowId) { | |||
| const inputValue = parseFloat(e.target.value) || 0; | |||
| const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); | |||
| {/* | |||
| // ✅ Validate input | |||
| if (inputValue > maxAllowed) { | |||
| // Set validation error for this lot | |||
| setValidationErrors(prev => ({ ...prev, [`lot_${lot.lotId}`]: `${t('Input quantity cannot exceed')} ${maxAllowed}` })); | |||
| return; | |||
| } else { | |||
| // Clear validation error if valid | |||
| setValidationErrors(prev => { | |||
| const newErrors = { ...prev }; | |||
| delete newErrors[`lot_${lot.lotId}`]; | |||
| return newErrors; | |||
| }); | |||
| */} | |||
| onPickQtyChange(selectedRowId, lot.lotId, inputValue); | |||
| } | |||
| }} | |||
| disabled={ | |||
| (lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || | |||
| selectedLotRowId !== `row_${index}` || | |||
| lot.stockOutLineStatus === 'completed' | |||
| } | |||
| error={!!validationErrors[`lot_${lot.lotId}`]} // ✅ Show red border when error | |||
| helperText={validationErrors[`lot_${lot.lotId}`]} // ✅ Show red error text below | |||
| inputProps={{ | |||
| min: 0, | |||
| max: calculateRemainingRequiredQty(lot), | |||
| step: 0.01 | |||
| }} | |||
| sx={{ | |||
| width: '60px', | |||
| height: '28px', | |||
| '& .MuiInputBase-input': { | |||
| fontSize: '0.7rem', | |||
| textAlign: 'center', | |||
| padding: '6px 8px' | |||
| } | |||
| }} | |||
| placeholder="0" | |||
| type="number" | |||
| size="small" | |||
| value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''} | |||
| onChange={(e) => { | |||
| if (selectedRowId) { | |||
| const inputValue = parseFloat(e.target.value) || 0; | |||
| const maxAllowed = Math.min(calculateRemainingAvailableQty(lot), calculateRemainingRequiredQty(lot)); | |||
| {/* | |||
| // ✅ Validate input | |||
| if (inputValue > maxAllowed) { | |||
| // Set validation error for this lot | |||
| setValidationErrors(prev => ({ ...prev, [`lot_${lot.lotId}`]: `${t('Input quantity cannot exceed')} ${maxAllowed}` })); | |||
| return; | |||
| } else { | |||
| // Clear validation error if valid | |||
| setValidationErrors(prev => { | |||
| const newErrors = { ...prev }; | |||
| delete newErrors[`lot_${lot.lotId}`]; | |||
| return newErrors; | |||
| }); | |||
| */} | |||
| onPickQtyChange(selectedRowId, lot.lotId, inputValue); | |||
| } | |||
| }} | |||
| disabled={ | |||
| (lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || | |||
| selectedLotRowId !== `row_${index}` || | |||
| lot.stockOutLineStatus === 'completed' | |||
| } | |||
| error={!!validationErrors[`lot_${lot.lotId}`]} // ✅ Show red border when error | |||
| helperText={validationErrors[`lot_${lot.lotId}`]} // ✅ Show red error text below | |||
| inputProps={{ | |||
| min: 0, | |||
| max: calculateRemainingRequiredQty(lot), | |||
| step: 0.01 | |||
| }} | |||
| sx={{ | |||
| width: '60px', | |||
| height: '28px', | |||
| '& .MuiInputBase-input': { | |||
| fontSize: '0.7rem', | |||
| textAlign: 'center', | |||
| padding: '6px 8px' | |||
| } | |||
| }} | |||
| placeholder="0" | |||
| /> | |||
| {/* ✅ 添加 Pick Form 按钮用于问题情况 */} | |||
| @@ -775,6 +775,12 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handlePickExecutionForm(lot)} | |||
| disabled={ | |||
| (lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || | |||
| selectedLotRowId !== `row_${index}` | |||
| } | |||
| sx={{ | |||
| fontSize: '0.7rem', | |||
| py: 0.5, | |||
| @@ -301,9 +301,9 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) && ( | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| @@ -343,8 +343,9 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| </FormControl> | |||
| </Grid> | |||
| </> | |||
| )} | |||
| </Grid> | |||
| ) : (<></>)} | |||
| </Grid> | |||
| </Box> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -227,6 +227,8 @@ | |||
| "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", | |||
| "Bad item Qty":"不良貨品數量", | |||
| "Missing item Qty":"缺少貨品數量", | |||
| "Bad Item Qty":"不良貨品數量", | |||
| "Missing Item Qty":"缺少貨品數量", | |||
| "Actual Pick Qty":"實際提料數量", | |||
| "Required Qty":"所需數量", | |||
| "Issue Remark":"問題描述", | |||
| @@ -234,5 +236,11 @@ | |||
| "Qty is required":"必需輸入數量", | |||
| "Qty is not allowed to be greater than remaining available qty":"輸入數量不能大於剩餘可用數量", | |||
| "Qty is not allowed to be greater than required qty":"輸入數量不能大於所需數量", | |||
| "At least one issue must be reported":"至少需要報告一個問題" | |||
| "At least one issue must be reported":"至少需要報告一個問題", | |||
| "issueRemark":"問題描述是必需的", | |||
| "handler":"處理者", | |||
| "Max":"最大值", | |||
| "Route":"路線", | |||
| "Index":"編號", | |||
| "No FG pick orders found":"沒有成品提料單" | |||
| } | |||