|
|
@@ -65,6 +65,14 @@ import { getCustomWidth } from "@/app/utils/commonUtil"; |
|
|
|
import PoInfoCard from "./PoInfoCard"; |
|
|
|
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { fetchPoListClient } from "@/app/api/po/actions"; |
|
|
|
import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material"; |
|
|
|
import { useRouter } from "next/navigation"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Props = { |
|
|
|
po: PoResult; |
|
|
|
qc: QcItemWithChecks[]; |
|
|
@@ -77,6 +85,89 @@ type EntryError = |
|
|
|
} |
|
|
|
| undefined; |
|
|
|
// type PolRow = TableRow<Partial<StockInLine>, EntryError>; |
|
|
|
const PoSearchList: React.FC<{ |
|
|
|
poList: PoResult[]; |
|
|
|
selectedPoId: number; |
|
|
|
onSelect: (po: PoResult) => void; |
|
|
|
}> = ({ poList, selectedPoId, onSelect }) => { |
|
|
|
const { t } = useTranslation("purchaseOrder"); |
|
|
|
const [searchTerm, setSearchTerm] = useState(''); |
|
|
|
|
|
|
|
const filteredPoList = useMemo(() => { |
|
|
|
if (searchTerm.trim() === '') { |
|
|
|
return poList; |
|
|
|
} |
|
|
|
return poList.filter(poItem => |
|
|
|
poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
|
poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
|
t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase()) |
|
|
|
); |
|
|
|
}, [poList, searchTerm, t]); |
|
|
|
|
|
|
|
return ( |
|
|
|
<Paper sx={{ p: 2, maxHeight: "400px", overflow: "auto" }}> |
|
|
|
<Typography variant="h6" gutterBottom> |
|
|
|
{t("Purchase Orders")} |
|
|
|
</Typography> |
|
|
|
<TextField |
|
|
|
label={t("Search")} |
|
|
|
variant="outlined" |
|
|
|
size="small" |
|
|
|
fullWidth |
|
|
|
value={searchTerm} |
|
|
|
onChange={(e) => setSearchTerm(e.target.value)} |
|
|
|
sx={{ mb: 2 }} |
|
|
|
InputProps={{ |
|
|
|
startAdornment: ( |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}> |
|
|
|
|
|
|
|
</Typography> |
|
|
|
), |
|
|
|
}} |
|
|
|
/> |
|
|
|
<List dense> |
|
|
|
{filteredPoList.map((poItem, index) => ( |
|
|
|
<div key={poItem.id}> |
|
|
|
<ListItem disablePadding> |
|
|
|
<ListItemButton |
|
|
|
selected={selectedPoId === poItem.id} |
|
|
|
onClick={() => onSelect(poItem)} |
|
|
|
sx={{ |
|
|
|
"&.Mui-selected": { |
|
|
|
backgroundColor: "primary.light", |
|
|
|
"&:hover": { |
|
|
|
backgroundColor: "primary.light", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}} |
|
|
|
> |
|
|
|
<ListItemText |
|
|
|
primary={ |
|
|
|
<Typography variant="body2" noWrap> |
|
|
|
{poItem.code} |
|
|
|
</Typography> |
|
|
|
} |
|
|
|
secondary={ |
|
|
|
<Typography variant="caption" color="text.secondary"> |
|
|
|
{t(`${poItem.status.toLowerCase()}`)} |
|
|
|
</Typography> |
|
|
|
} |
|
|
|
/> |
|
|
|
</ListItemButton> |
|
|
|
</ListItem> |
|
|
|
{index < filteredPoList.length - 1 && <Divider />} |
|
|
|
</div> |
|
|
|
))} |
|
|
|
</List> |
|
|
|
{searchTerm && ( |
|
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}> |
|
|
|
{t("Found")} {filteredPoList.length} {t("of")} {poList.length} {t("items")} |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
</Paper> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { |
|
|
|
const cameras = useContext(CameraContext); |
|
|
|
console.log(cameras); |
|
|
@@ -93,6 +184,90 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { |
|
|
|
const searchParams = useSearchParams(); |
|
|
|
// const [currPoStatus, setCurrPoStatus] = useState(purchaseOrder.status); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter(); |
|
|
|
const [poList, setPoList] = useState<PoResult[]>([]); |
|
|
|
const [selectedPoId, setSelectedPoId] = useState(po.id); |
|
|
|
const currentPoId = searchParams.get('id'); |
|
|
|
const [searchTerm, setSearchTerm] = useState(''); |
|
|
|
|
|
|
|
|
|
|
|
const filteredPoList = useMemo(() => { |
|
|
|
if (searchTerm.trim() === '') { |
|
|
|
return poList; |
|
|
|
} |
|
|
|
return poList.filter(poItem => |
|
|
|
poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
|
poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) || |
|
|
|
t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase()) |
|
|
|
); |
|
|
|
}, [poList, searchTerm, t]); |
|
|
|
|
|
|
|
|
|
|
|
const fetchPoList = useCallback(async () => { |
|
|
|
try { |
|
|
|
const result = await fetchPoListClient({ limit: 20, offset: 0 }); |
|
|
|
if (result && result.records) { |
|
|
|
setPoList(result.records); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("Failed to fetch PO list:", error); |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
const handlePoSelect = useCallback( |
|
|
|
(selectedPo: PoResult) => { |
|
|
|
setSelectedPoId(selectedPo.id); |
|
|
|
router.push(`/po/edit?id=${selectedPo.id}&start=true`, { scroll: false }); |
|
|
|
}, |
|
|
|
[router] |
|
|
|
); |
|
|
|
|
|
|
|
const fetchPoDetail = useCallback(async (poId: string) => { |
|
|
|
try { |
|
|
|
const result = await fetchPoInClient(parseInt(poId)); |
|
|
|
if (result) { |
|
|
|
setPurchaseOrder(result); |
|
|
|
setRows(result.pol || []); |
|
|
|
if (result.pol && result.pol.length > 0) { |
|
|
|
setRow(result.pol[0]); |
|
|
|
setStockInLine(result.pol[0].stockInLine); |
|
|
|
setProcessedQty(result.pol[0].processed); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("Failed to fetch PO detail:", error); |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (currentPoId && currentPoId !== selectedPoId.toString()) { |
|
|
|
setSelectedPoId(parseInt(currentPoId)); |
|
|
|
fetchPoDetail(currentPoId); |
|
|
|
} |
|
|
|
}, [currentPoId, selectedPoId, fetchPoDetail]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
fetchPoList(); |
|
|
|
}, [fetchPoList]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (currentPoId) { |
|
|
|
setSelectedPoId(parseInt(currentPoId)); |
|
|
|
} |
|
|
|
}, [currentPoId]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const removeParam = (paramToRemove: string) => { |
|
|
|
const newParams = new URLSearchParams(searchParams.toString()); |
|
|
|
newParams.delete(paramToRemove); |
|
|
@@ -315,116 +490,84 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { |
|
|
|
|
|
|
|
return ( |
|
|
|
<> |
|
|
|
<Stack |
|
|
|
spacing={2} |
|
|
|
// component="form" |
|
|
|
// onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} |
|
|
|
> |
|
|
|
<Stack spacing={2}> |
|
|
|
{/* Area1: title */} |
|
|
|
<Grid container xs={12} justifyContent="start"> |
|
|
|
<Grid item> |
|
|
|
<Typography mb={2} variant="h4"> |
|
|
|
{/* {purchaseOrder.code} - {currPoStatus} */} |
|
|
|
{purchaseOrder.code} -{" "} |
|
|
|
{t(`${purchaseOrder.status.toLowerCase()}`)} |
|
|
|
</Typography> |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
{true ? (<Grid container xs={12} justifyContent="start"> |
|
|
|
<Grid item xs={3}> |
|
|
|
<TextField |
|
|
|
label={t("dnNo")} |
|
|
|
type="text" // Use type="text" to allow validation in the change handler |
|
|
|
variant="outlined" |
|
|
|
// value={secondReceiveQty} |
|
|
|
// onChange={handleChange} |
|
|
|
InputProps={{ |
|
|
|
inputProps: { |
|
|
|
min: 0, // Optional: set a minimum value |
|
|
|
step: 1 // Optional: set the step for the number input |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
{/* area2: dn info */} |
|
|
|
<Grid container spacing={2}> |
|
|
|
{/* left side select po */} |
|
|
|
<Grid item xs={3}> |
|
|
|
<TextField |
|
|
|
label={t("dnDate")} |
|
|
|
type="text" // Use type="text" to allow validation in the change handler |
|
|
|
variant="outlined" |
|
|
|
defaultValue={"11/08/2025"} |
|
|
|
// value={secondReceiveQty} |
|
|
|
// onChange={handleChange} |
|
|
|
InputProps={{ |
|
|
|
inputProps: { |
|
|
|
min: 0, // Optional: set a minimum value |
|
|
|
step: 1 // Optional: set the step for the number input |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
{/* <Button |
|
|
|
onClick={buttonData.onClick} |
|
|
|
disabled={buttonData.disabled} |
|
|
|
color={buttonData.buttonColor as ButtonProps["color"]} |
|
|
|
startIcon={buttonData.buttonIcon} |
|
|
|
> |
|
|
|
{buttonData.buttonText} |
|
|
|
</Button> */} |
|
|
|
</Grid> |
|
|
|
<Grid |
|
|
|
item |
|
|
|
xs={6} |
|
|
|
display="flex" |
|
|
|
// justifyContent="center" |
|
|
|
alignItems="center" |
|
|
|
> |
|
|
|
<Button variant={"contained"} onClick={onOpenScanner}>提交</Button> |
|
|
|
</Grid> |
|
|
|
</Grid>) : undefined} |
|
|
|
<Grid container xs={12} justifyContent="space-between"> |
|
|
|
|
|
|
|
{/* <Grid item xs={4}> */} |
|
|
|
{/* scanner */} |
|
|
|
{/* </Grid> */} |
|
|
|
<Grid |
|
|
|
item |
|
|
|
xs={4} |
|
|
|
display="flex" |
|
|
|
justifyContent="end" |
|
|
|
alignItems="end" |
|
|
|
> |
|
|
|
<QrModal |
|
|
|
open={isOpenScanner} |
|
|
|
onClose={onCloseScanner} |
|
|
|
warehouse={warehouse} |
|
|
|
<PoSearchList |
|
|
|
poList={poList} |
|
|
|
selectedPoId={selectedPoId} |
|
|
|
onSelect={handlePoSelect} |
|
|
|
/> |
|
|
|
{/* <Button onClick={onOpenScanner}>{t("Accept submit")}</Button> */} |
|
|
|
{/* <Button onClick={onOpenScanner}>{t("bind")}</Button> */} |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
{/* tab 1 */} |
|
|
|
<Grid rowSpacing={2} container sx={{ display: tabIndex === 0 ? "block" : "none" }}> |
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
|
|
{/* right side po info */} |
|
|
|
<Grid item xs={9}> |
|
|
|
<PoInfoCard po={purchaseOrder} /> |
|
|
|
{true ? ( |
|
|
|
<Stack spacing={2}> |
|
|
|
<TextField |
|
|
|
label={t("dnNo")} |
|
|
|
type="text" |
|
|
|
variant="outlined" |
|
|
|
fullWidth |
|
|
|
InputProps={{ |
|
|
|
inputProps: { |
|
|
|
min: 0, |
|
|
|
step: 1 |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
<TextField |
|
|
|
label={t("dnDate")} |
|
|
|
type="text" |
|
|
|
variant="outlined" |
|
|
|
defaultValue={"11/08/2025"} |
|
|
|
fullWidth |
|
|
|
InputProps={{ |
|
|
|
inputProps: { |
|
|
|
min: 0, |
|
|
|
step: 1 |
|
|
|
} |
|
|
|
}} |
|
|
|
/> |
|
|
|
<Button variant="contained" onClick={onOpenScanner} fullWidth> |
|
|
|
提交 |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
) : undefined} |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Area4: Main Table */} |
|
|
|
<Grid container xs={12} justifyContent="start"> |
|
|
|
<Grid item xs={12}> |
|
|
|
<TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}> |
|
|
|
{/* <TableContainer component={Paper} sx={{width: getCustomWidth(), overflow: 'auto' }}> */} |
|
|
|
<Table aria-label="collapsible table" stickyHeader> |
|
|
|
<TableHead> |
|
|
|
<TableRow> |
|
|
|
{/* <TableCell /> for the collapse button */} |
|
|
|
<TableCell sx={{ width: '125px' }}>{t("itemNo")}</TableCell> |
|
|
|
<TableCell align="left" sx={{ width: '125px' }}>{t("itemName")}</TableCell> |
|
|
|
<TableCell align="right">{t("qty")}</TableCell> |
|
|
|
<TableCell align="right">{t("processed")}</TableCell> |
|
|
|
<TableCell align="left">{t("uom")}</TableCell> |
|
|
|
<TableCell align="right">{t("total weight")}</TableCell> |
|
|
|
{/* <TableCell align="left">{t("weight unit")}</TableCell> */} |
|
|
|
<TableCell align="right">{`${t("price")} (HKD)`}</TableCell> |
|
|
|
{/* <TableCell align="left">{t("expiryDate")}</TableCell> */} |
|
|
|
<TableCell align="left"sx={{ width: '75px' }}>{t("status")}</TableCell> |
|
|
|
{/* start == true && firstInQty == null ? no hide : hide*/} |
|
|
|
<TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell> |
|
|
|
{renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} |
|
|
|
{/* start == true && firstInQty == null ? hide and disabled : no hide*/} |
|
|
|
{renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}(以訂單單位計算)</TableCell> : undefined} |
|
|
|
<TableCell align="center" sx={{ width: '100px' }}></TableCell> |
|
|
|
</TableRow> |
|
|
@@ -437,6 +580,10 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { |
|
|
|
</Table> |
|
|
|
</TableContainer> |
|
|
|
</Grid> |
|
|
|
</Grid> |
|
|
|
|
|
|
|
{/* area5: selected item info */} |
|
|
|
<Grid container xs={12} justifyContent="start"> |
|
|
|
<Grid item xs={12}> |
|
|
|
<Typography variant="h6">已選擇: {row.itemNo}-{row.itemName}</Typography> |
|
|
|
</Grid> |
|
|
@@ -486,4 +633,4 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { |
|
|
|
</> |
|
|
|
); |
|
|
|
}; |
|
|
|
export default PoDetail; |
|
|
|
export default PoDetail; |