FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

523 řádky
17 KiB

  1. "use client";
  2. import {
  3. StockInInput,
  4. } from "@/app/api/stockIn/actions";
  5. import {
  6. Box,
  7. Button,
  8. Card,
  9. CardContent,
  10. Grid,
  11. InputAdornment,
  12. Stack,
  13. TextField,
  14. Tooltip,
  15. Typography,
  16. } from "@mui/material";
  17. import { Controller, useFormContext } from "react-hook-form";
  18. import { useTranslation } from "react-i18next";
  19. import StyledDataGrid from "../StyledDataGrid";
  20. import { useCallback, useEffect, useState, useMemo } from "react";
  21. import {
  22. useGridApiRef,
  23. } from "@mui/x-data-grid";
  24. import { StockInLine } from "@/app/api/stockIn";
  25. import {
  26. needsPoQcStockQtyRound,
  27. roundStockQty,
  28. StockQtyRoundMode,
  29. } from "@/components/PoDetail/stockQtyRound";
  30. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  31. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  32. import { dayjsToDateString, INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  33. import dayjs from "dayjs";
  34. import CalculateExpiryDateModal from "./CalculateExpiryDateModal";
  35. // change PurchaseQcResult to stock in entry props
  36. interface Props {
  37. itemDetail: StockInLine;
  38. // qc: QcItemWithChecks[];
  39. disabled: boolean;
  40. putawayMode?: boolean;
  41. /** When true, use compact field sizing to match Putaway Detail (上架詳情) section. */
  42. compactFields?: boolean;
  43. }
  44. type EntryError =
  45. | {
  46. [field in keyof StockInInput]?: string;
  47. }
  48. | undefined;
  49. // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>;
  50. const textfieldSx = {
  51. width: "100%",
  52. "& .MuiInputBase-root": {
  53. // height: "120", // Scales with root font size
  54. height: "5rem", // Scales with root font size
  55. },
  56. "& .MuiInputBase-input": {
  57. height: "100%",
  58. boxSizing: "border-box",
  59. padding: "0.75rem",
  60. fontSize: 30,
  61. },
  62. "& .MuiInputLabel-root": {
  63. fontSize: 30,
  64. transform: "translate(14px, 1.2rem) scale(1)",
  65. "&.MuiInputLabel-shrink": {
  66. fontSize: 24,
  67. transform: "translate(14px, -0.5rem) scale(1)",
  68. },
  69. // [theme.breakpoints.down("sm")]: {
  70. // fontSize: "1rem",
  71. // transform: "translate(14px, 1.5rem) scale(1)",
  72. // "&.MuiInputLabel-shrink": {
  73. // fontSize: "0.875rem",
  74. // },
  75. // },
  76. },
  77. };
  78. const StockInForm: React.FC<Props> = ({
  79. // qc,
  80. itemDetail,
  81. disabled,
  82. putawayMode = false,
  83. compactFields = false,
  84. }) => {
  85. const {
  86. t,
  87. i18n: { language },
  88. } = useTranslation("purchaseOrder");
  89. const apiRef = useGridApiRef();
  90. const {
  91. register,
  92. formState: { errors, defaultValues, touchedFields },
  93. watch,
  94. control,
  95. setValue,
  96. getValues,
  97. reset,
  98. resetField,
  99. setError,
  100. clearErrors,
  101. } = useFormContext<StockInInput>();
  102. // console.log(itemDetail);
  103. useEffect(() => {
  104. // console.log("triggered");
  105. // // receiptDate default tdy
  106. // setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT));
  107. // setValue("status", "received");
  108. }, [setValue]);
  109. useEffect(() => {
  110. console.log(errors);
  111. }, [errors]);
  112. const productionDate = watch("productionDate");
  113. const expiryDate = watch("expiryDate");
  114. const uom = watch("uom");
  115. const stockQtyRoundMode = watch("stockQtyRoundMode");
  116. const displayedAcceptedQty = watch("acceptedQty");
  117. const originalAcceptedQty = Number(itemDetail.acceptedQty ?? 0);
  118. const showStockQtyRoundButtons =
  119. !putawayMode &&
  120. !disabled &&
  121. needsPoQcStockQtyRound(
  122. itemDetail.purchaseOrderLineId,
  123. itemDetail.status,
  124. originalAcceptedQty,
  125. );
  126. const stockUomLabel = itemDetail.stockUomDesc ? ` ${itemDetail.stockUomDesc}` : "";
  127. const roundUpQty = roundStockQty(originalAcceptedQty, "CEILING");
  128. const roundDownQty = roundStockQty(originalAcceptedQty, "FLOOR");
  129. const selectStockQtyRound = useCallback(
  130. (mode: StockQtyRoundMode) => {
  131. const after = roundStockQty(Number(itemDetail.acceptedQty ?? 0), mode);
  132. setValue("stockQtyRoundMode", mode, { shouldDirty: true, shouldTouch: true });
  133. setValue("acceptedQty", after, { shouldDirty: true, shouldTouch: true });
  134. (setValue as (name: string, value: unknown, options?: object) => void)(
  135. "acceptQty",
  136. after,
  137. { shouldDirty: true, shouldTouch: true },
  138. );
  139. const accInput = document.getElementById("accQty") as HTMLInputElement | null;
  140. if (accInput) accInput.value = String(after);
  141. },
  142. [itemDetail.acceptedQty, setValue],
  143. );
  144. const [openModal, setOpenModal] = useState<boolean>(false);
  145. const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false);
  146. //// TODO : Add Checking ////
  147. // Check if dates are input
  148. // if (data.expiryDate === undefined || data.expiryDate == null) {
  149. // validationErrors.push("請輸入到期日!");
  150. // }
  151. useEffect(() => {
  152. // console.log(uom);
  153. // console.log(productionDate);
  154. // console.log(expiryDate);
  155. if (expiryDate) clearErrors();
  156. }, [productionDate, expiryDate, clearErrors]);
  157. useEffect(() => {
  158. console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail);
  159. }, [itemDetail]);
  160. const handleOpenModal = useCallback(() => {
  161. setOpenModal(true);
  162. }, []);
  163. const handleOnModalClose = useCallback(() => {
  164. setOpenExpDatePicker(false);
  165. setOpenModal(false);
  166. }, []);
  167. const handleReturnExpiryDate = useCallback((result: dayjs.Dayjs) => {
  168. if (result) {
  169. setValue("expiryDate", dayjsToDateString(result));
  170. }
  171. }, []);
  172. return (
  173. <>
  174. <Grid container justifyContent="flex-start" alignItems="flex-start">
  175. {/* <Grid item xs={12}>
  176. <Typography variant="h6" display="block" marginBlockEnd={1}>
  177. {t("Stock In Detail")}
  178. </Typography>
  179. </Grid> */}
  180. <Grid
  181. container
  182. justifyContent="flex-start"
  183. alignItems="flex-start"
  184. spacing={compactFields ? 1 : 2}
  185. sx={{ mt: 0.5 }}
  186. >
  187. <Grid item xs={6}>
  188. <TextField
  189. label={t("dnNo")}
  190. fullWidth
  191. {...register("dnNo", {
  192. // required: "productLotNo required!",
  193. })}
  194. sx={compactFields ? undefined : textfieldSx}
  195. disabled={disabled}
  196. // error={Boolean(errors.productLotNo)}
  197. // helperText={errors.productLotNo?.message}
  198. />
  199. </Grid>
  200. <Grid item xs={6}>
  201. <TextField
  202. label={t("itemName")}
  203. fullWidth
  204. {...register("itemName", {
  205. // required: "productLotNo required!",
  206. })}
  207. sx={compactFields ? undefined : textfieldSx}
  208. disabled={true}
  209. // error={Boolean(errors.productLotNo)}
  210. // helperText={errors.productLotNo?.message}
  211. />
  212. </Grid>
  213. <Grid item xs={6}>
  214. <TextField
  215. label={t("PO No.")}
  216. fullWidth
  217. {...register("poCode", {
  218. // required: "productLotNo required!",
  219. })}
  220. sx={compactFields ? undefined : textfieldSx}
  221. disabled={true}
  222. // error={Boolean(errors.productLotNo)}
  223. // helperText={errors.productLotNo?.message}
  224. />
  225. </Grid>
  226. {putawayMode || (
  227. <>
  228. <Grid item xs={6}>
  229. <Controller
  230. control={control}
  231. name="receiptDate"
  232. rules={{ required: true }}
  233. render={({ field }) => {
  234. return (
  235. <LocalizationProvider
  236. dateAdapter={AdapterDayjs}
  237. adapterLocale={`${language}-hk`}
  238. >
  239. <DatePicker
  240. {...field}
  241. sx={compactFields ? { width: '100%' } : textfieldSx}
  242. label={t("receiptDate")}
  243. value={dayjs(watch("receiptDate"))}
  244. format={OUTPUT_DATE_FORMAT}
  245. disabled={true}
  246. onChange={(date) => {
  247. if (!date) return;
  248. // setValue("receiptDate", date.format(INPUT_DATE_FORMAT));
  249. field.onChange(date);
  250. }}
  251. inputRef={field.ref}
  252. slotProps={{
  253. textField: {
  254. fullWidth: true,
  255. // required: true,
  256. error: Boolean(errors.receiptDate?.message),
  257. helperText: errors.receiptDate?.message,
  258. },
  259. }}
  260. />
  261. </LocalizationProvider>
  262. );
  263. }}
  264. />
  265. </Grid>
  266. <Grid item xs={6}>
  267. <TextField
  268. label={t("Supplier")}
  269. fullWidth
  270. {...register("supplier", {
  271. // required: "productLotNo required!",
  272. })}
  273. sx={compactFields ? undefined : textfieldSx}
  274. disabled={true}
  275. />
  276. </Grid>
  277. </>
  278. )}
  279. <Grid item xs={6}>
  280. {putawayMode ? (
  281. <TextField
  282. label={t("stockLotNo")}
  283. fullWidth
  284. {...register("lotNo", {
  285. // required: "productLotNo required!",
  286. })}
  287. sx={compactFields ? undefined : textfieldSx}
  288. disabled={disabled}
  289. error={Boolean(errors.productLotNo)}
  290. helperText={errors.productLotNo?.message}
  291. />) : (
  292. <TextField
  293. label={t("productLotNo")}
  294. fullWidth
  295. {...register("productLotNo", {
  296. // required: "productLotNo required!",
  297. })}
  298. sx={compactFields ? undefined : textfieldSx}
  299. disabled={disabled}
  300. error={Boolean(errors.productLotNo)}
  301. helperText={errors.productLotNo?.message}
  302. />)}
  303. </Grid>
  304. <Grid item xs={6}>
  305. <Controller
  306. control={control}
  307. name="expiryDate"
  308. // rules={{ required: !Boolean(productionDate) }}
  309. render={({ field }) => {
  310. return (
  311. <LocalizationProvider
  312. dateAdapter={AdapterDayjs}
  313. adapterLocale={`${language}-hk`}
  314. >
  315. <DatePicker
  316. {...field}
  317. sx={compactFields ? { width: '100%' } : textfieldSx}
  318. label={t("expiryDate")}
  319. value={expiryDate ? dayjs(expiryDate) : undefined}
  320. format={OUTPUT_DATE_FORMAT}
  321. disabled={disabled}
  322. onChange={(date) => {
  323. if (!date) return;
  324. console.log(date.format(INPUT_DATE_FORMAT));
  325. setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
  326. // field.onChange(date);
  327. }}
  328. inputRef={field.ref}
  329. open={openExpDatePicker && !openModal}
  330. onOpen={() => setOpenExpDatePicker(true)}
  331. onClose={() => setOpenExpDatePicker(false)}
  332. slotProps={{
  333. textField: {
  334. fullWidth: true,
  335. InputProps: {
  336. ...(!disabled && {endAdornment: (
  337. <InputAdornment position="end">
  338. <Button
  339. type="button"
  340. variant="contained"
  341. color="primary"
  342. size="small"
  343. sx={{
  344. fontSize: compactFields ? '0.875rem' : '24px',
  345. whiteSpace: 'nowrap',
  346. flexShrink: 0,
  347. }}
  348. onClick={handleOpenModal}
  349. >
  350. {t("Calculate Expiry Date")}
  351. </Button>
  352. </InputAdornment>
  353. ),})
  354. },
  355. // required: true,
  356. error: Boolean(errors.expiryDate?.message),
  357. helperText: errors.expiryDate?.message,
  358. onClick: () => setOpenExpDatePicker(true),
  359. },
  360. }}
  361. />
  362. </LocalizationProvider>
  363. );
  364. }}
  365. />
  366. </Grid>
  367. {putawayMode || (<>
  368. <Grid item xs={3}>
  369. <TextField
  370. label={t("receivedQty")}
  371. fullWidth
  372. {...register("receivedQty", {
  373. required: "receivedQty required!",
  374. })}
  375. sx={compactFields ? undefined : textfieldSx}
  376. disabled={true}
  377. />
  378. </Grid>
  379. <Grid item xs={3}>
  380. <TextField
  381. label={t("qty")}
  382. fullWidth
  383. {...register("qty", {
  384. required: "qty required!",
  385. })}
  386. sx={compactFields ? undefined : textfieldSx}
  387. disabled={true}
  388. />
  389. </Grid></>
  390. )}
  391. {putawayMode ? (
  392. <>
  393. <Grid item xs={6}>
  394. <TextField
  395. label={"本批次庫存收貨數量"}
  396. fullWidth
  397. sx={compactFields ? undefined : textfieldSx}
  398. disabled={true}
  399. value={`${itemDetail.acceptedQty ?? 0} (${itemDetail.stockUomDesc ?? ""})`}
  400. />
  401. </Grid>
  402. <Grid item xs={6}>
  403. <TextField
  404. label={"本批次採購收貨數量"}
  405. fullWidth
  406. sx={compactFields ? undefined : textfieldSx}
  407. disabled={true}
  408. value={`${itemDetail.purchaseAcceptedQty ?? 0} (${itemDetail.purchaseUomDesc ?? ""})`}
  409. />
  410. </Grid>
  411. <Grid item xs={6}>
  412. <TextField
  413. label={"已上架數量"}
  414. fullWidth
  415. sx={compactFields ? undefined : textfieldSx}
  416. disabled={true}
  417. value={`${itemDetail.putAwayLines?.reduce((sum, p) => sum + (p.stockQty ?? 0), 0) ?? 0} (${itemDetail.stockUomDesc ?? ""})`}
  418. />
  419. </Grid>
  420. </>
  421. ) : (
  422. <>
  423. <Grid item xs={6}>
  424. <TextField
  425. label={t("acceptedQty")}
  426. fullWidth
  427. sx={compactFields ? undefined : textfieldSx}
  428. disabled={true}
  429. value={displayedAcceptedQty ?? originalAcceptedQty}
  430. {...register("acceptedQty", {
  431. required: "acceptedQty required!",
  432. })}
  433. />
  434. </Grid>
  435. {showStockQtyRoundButtons && (
  436. <Grid item xs={6}>
  437. <Stack spacing={0.75} justifyContent="flex-end" sx={{ height: "100%" }}>
  438. <Typography variant="caption" color="text.secondary">
  439. {t("Please choose a rounding method")}
  440. </Typography>
  441. <Stack direction="row" spacing={1} alignItems="stretch">
  442. <Button
  443. variant={stockQtyRoundMode === "CEILING" ? "contained" : "outlined"}
  444. size={compactFields ? "medium" : "large"}
  445. onClick={() => selectStockQtyRound("CEILING")}
  446. sx={{ flex: 1, fontWeight: 700 }}
  447. >
  448. {t("Round ceiling")} {roundUpQty}{stockUomLabel}
  449. </Button>
  450. <Button
  451. variant={stockQtyRoundMode === "FLOOR" ? "contained" : "outlined"}
  452. size={compactFields ? "medium" : "large"}
  453. onClick={() => selectStockQtyRound("FLOOR")}
  454. sx={{ flex: 1, fontWeight: 700 }}
  455. >
  456. {t("Round floor")} {roundDownQty}{stockUomLabel}
  457. </Button>
  458. </Stack>
  459. </Stack>
  460. </Grid>
  461. )}
  462. </>
  463. )}
  464. {/* <Grid item xs={4}>
  465. <TextField
  466. label={t("acceptedWeight")}
  467. fullWidth
  468. // {...register("acceptedWeight", {
  469. // required: "acceptedWeight required!",
  470. // })}
  471. disabled={disabled}
  472. error={Boolean(errors.acceptedWeight)}
  473. helperText={errors.acceptedWeight?.message}
  474. />
  475. </Grid> */}
  476. </Grid>
  477. <Grid
  478. container
  479. justifyContent="flex-start"
  480. alignItems="flex-start"
  481. spacing={2}
  482. sx={{ mt: 0.5 }}
  483. >
  484. {/* <Grid item xs={12}>
  485. <InputDataGrid<PurchaseQCInput, PurchaseQcResult, EntryError>
  486. apiRef={apiRef}
  487. checkboxSelection={false}
  488. _formKey={"qcCheck"}
  489. columns={columns}
  490. validateRow={validationTest}
  491. />
  492. </Grid> */}
  493. </Grid>
  494. </Grid>
  495. <CalculateExpiryDateModal
  496. open={openModal}
  497. onClose={handleOnModalClose}
  498. onSubmit={handleReturnExpiryDate}
  499. textfieldSx={compactFields ? undefined : textfieldSx}
  500. />
  501. </>
  502. );
  503. };
  504. export default StockInForm;