FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

PoDetail.tsx 32 KiB

6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3週間前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
6ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. "use client";
  2. import {
  3. fetchPoWithStockInLines,
  4. PoResult,
  5. PurchaseOrderLine,
  6. } from "@/app/api/po";
  7. import {
  8. Box,
  9. Button,
  10. ButtonProps,
  11. Collapse,
  12. Grid,
  13. IconButton,
  14. Paper,
  15. Stack,
  16. Tab,
  17. Table,
  18. TableBody,
  19. TableCell,
  20. TableContainer,
  21. TableHead,
  22. TableRow,
  23. Tabs,
  24. TabsProps,
  25. TextField,
  26. Typography,
  27. Checkbox,
  28. FormControlLabel,
  29. Card,
  30. CardContent,
  31. Radio,
  32. } from "@mui/material";
  33. import { useTranslation } from "react-i18next";
  34. // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
  35. import {
  36. GridColDef,
  37. GridRowId,
  38. GridRowModel,
  39. useGridApiRef,
  40. } from "@mui/x-data-grid";
  41. import {
  42. checkPolAndCompletePo,
  43. fetchPoInClient,
  44. fetchPoListClient,
  45. startPo,
  46. } from "@/app/api/po/actions";
  47. import {
  48. createStockInLine
  49. } from "@/app/api/stockIn/actions";
  50. import {
  51. useCallback,
  52. useContext,
  53. useEffect,
  54. useMemo,
  55. useState,
  56. } from "react";
  57. import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
  58. import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
  59. import PoInputGrid from "./PoInputGrid";
  60. // import { QcItemWithChecks } from "@/app/api/qc";
  61. import { useRouter, useSearchParams, usePathname } from "next/navigation";
  62. import { WarehouseResult } from "@/app/api/warehouse";
  63. import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
  64. import { CameraContext } from "../Cameras/CameraProvider";
  65. import QrModal from "./QrModal";
  66. import { PlayArrow } from "@mui/icons-material";
  67. import DoneIcon from "@mui/icons-material/Done";
  68. import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
  69. import PoInfoCard from "./PoInfoCard";
  70. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  71. import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
  72. import { Controller, FormProvider, useForm } from "react-hook-form";
  73. import dayjs, { Dayjs } from "dayjs";
  74. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  75. import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
  76. import { debounce } from "lodash";
  77. import LoadingComponent from "../General/LoadingComponent";
  78. import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions";
  79. import { PrinterCombo } from "@/app/api/settings/printer";
  80. import { EscalationCombo } from "@/app/api/user";
  81. import { StockInLine } from "@/app/api/stockIn";
  82. //import { useRouter } from "next/navigation";
  83. type Props = {
  84. po: PoResult;
  85. // qc: QcItemWithChecks[];
  86. warehouse: WarehouseResult[];
  87. printerCombo: PrinterCombo[];
  88. };
  89. type EntryError =
  90. | {
  91. [field in keyof StockInLine]?: string;
  92. }
  93. | undefined;
  94. // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
  95. const PoSearchList: React.FC<{
  96. poList: PoResult[];
  97. selectedPoId: number;
  98. onSelect: (po: PoResult) => void;
  99. }> = ({ poList, selectedPoId, onSelect }) => {
  100. const { t } = useTranslation(["purchaseOrder", "dashboard"]);
  101. const [searchTerm, setSearchTerm] = useState('');
  102. const filteredPoList = useMemo(() => {
  103. if (searchTerm.trim() === '') {
  104. return poList;
  105. }
  106. return poList.filter(poItem =>
  107. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  108. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  109. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  110. );
  111. }, [poList, searchTerm, t]);
  112. return (
  113. <Paper sx={{ p: 2, maxHeight: "480px", overflow: "auto", minWidth: "300px", height: "480px" }}>
  114. <Typography variant="h6" gutterBottom>
  115. {t("Purchase Order")}
  116. </Typography>
  117. <TextField
  118. label={t("Search")}
  119. variant="outlined"
  120. size="small"
  121. fullWidth
  122. value={searchTerm}
  123. onChange={(e) => setSearchTerm(e.target.value)}
  124. sx={{ mb: 2 }}
  125. InputProps={{
  126. startAdornment: (
  127. <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
  128. </Typography>
  129. ),
  130. }}
  131. />
  132. {(filteredPoList.length > 0)? (
  133. <List dense sx={{ width: '100%' }}>
  134. {filteredPoList.map((poItem, index) => (
  135. <div key={poItem.id}>
  136. <ListItem disablePadding sx={{ width: '100%' }}>
  137. <ListItemButton
  138. selected={selectedPoId === poItem.id}
  139. onClick={() => onSelect(poItem)}
  140. sx={{
  141. width: '100%',
  142. "&.Mui-selected": {
  143. backgroundColor: "primary.light",
  144. "&:hover": {
  145. backgroundColor: "primary.light",
  146. },
  147. },
  148. }}
  149. >
  150. <ListItemText
  151. primary={
  152. <Typography variant="body2" sx={{ wordBreak: 'break-all' }}>
  153. {poItem.code}
  154. </Typography>
  155. }
  156. secondary={
  157. <Typography variant="caption" color="text.secondary">
  158. {t(`${poItem.status.toLowerCase()}`)}
  159. </Typography>
  160. }
  161. />
  162. </ListItemButton>
  163. </ListItem>
  164. {index < filteredPoList.length - 1 && <Divider />}
  165. </div>
  166. ))}
  167. </List>) : (
  168. <LoadingComponent/>
  169. )
  170. }
  171. {searchTerm && (
  172. <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
  173. {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
  174. {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */}
  175. </Typography>
  176. )}
  177. </Paper>
  178. );
  179. };
  180. interface PolInputResult {
  181. lotNo: string,
  182. dnQty: number,
  183. }
  184. const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
  185. const cameras = useContext(CameraContext);
  186. // console.log(cameras);
  187. const { t } = useTranslation("purchaseOrder");
  188. const apiRef = useGridApiRef();
  189. const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
  190. const [rows, setRows] = useState<PurchaseOrderLine[]>(
  191. purchaseOrder.pol || [],
  192. );
  193. const [polInputList, setPolInputList] = useState<PolInputResult[]>([])
  194. useEffect(() => {
  195. setPolInputList(
  196. (purchaseOrder.pol ?? []).map(() => ({
  197. lotNo: "",
  198. dnQty: 0,
  199. } as PolInputResult))
  200. );
  201. }, [purchaseOrder.pol]);
  202. const pathname = usePathname()
  203. const searchParams = useSearchParams();
  204. const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);
  205. const defaultPolId = searchParams.get("polId")
  206. useEffect(() => {
  207. if (defaultPolId) {
  208. setSelectedRow(rows.find((r) => r.id.toString() === defaultPolId) ?? null)
  209. console.log("%c StockIn:", "color:green", selectedRow);
  210. }
  211. }, [])
  212. const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
  213. const [processedQty, setProcessedQty] = useState(0);
  214. const router = useRouter();
  215. const [poList, setPoList] = useState<PoResult[]>([]);
  216. const [selectedPoId, setSelectedPoId] = useState(po.id);
  217. const [focusField, setFocusField] = useState<HTMLInputElement>();
  218. const currentPoId = searchParams.get('id');
  219. const selectedIdsParam = searchParams.get('selectedIds');
  220. // const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  221. const dnFormProps = useForm({
  222. defaultValues: {
  223. dnNo: '',
  224. receiptDate: dayjsToDateString(dayjs())
  225. }
  226. })
  227. const fetchPoList = useCallback(async () => {
  228. try {
  229. if (selectedIdsParam) {
  230. const selectedIds = selectedIdsParam.split(',').map(id => parseInt(id));
  231. const promises = selectedIds.map(id => fetchPoInClient(id));
  232. const results = await Promise.all(promises);
  233. setPoList(results.filter(Boolean));
  234. } else {
  235. const result = await fetchPoListClient({ limit: 20, offset: 0 });
  236. if (result && result.records) {
  237. setPoList(result.records);
  238. }
  239. }
  240. } catch (error) {
  241. console.error("Failed to fetch PO list:", error);
  242. }
  243. }, [selectedIdsParam]);
  244. const fetchPoDetail = useCallback(async (poId: string) => {
  245. try {
  246. const result = await fetchPoInClient(parseInt(poId));
  247. if (result) {
  248. console.log("%c Fetched PO:", "color:orange", result);
  249. setPurchaseOrder(result);
  250. setRows(result.pol || []);
  251. if (result.pol && result.pol.length > 0) {
  252. if (result.id === selectedPoId) {
  253. const polIndex = result.pol.findIndex((p) => p.id === selectedRow?.id)
  254. // setSelectedRow(result.pol[polIndex]);
  255. setStockInLine(result.pol[polIndex].stockInLine);
  256. setProcessedQty(result.pol[polIndex].processed);
  257. } else {
  258. // setSelectedRow(result.pol[0]);
  259. setStockInLine(result.pol[0].stockInLine);
  260. setProcessedQty(result.pol[0].processed);
  261. }
  262. }
  263. // if (focusField) {console.log(focusField);focusField.focus();}
  264. }
  265. } catch (error) {
  266. console.error("Failed to fetch PO detail:", error);
  267. }
  268. }, [selectedRow]);
  269. const handlePoSelect = useCallback(
  270. async (selectedPo: PoResult) => {
  271. if (selectedPo.id === selectedPoId) return;
  272. setSelectedPoId(selectedPo.id);
  273. await fetchPoDetail(selectedPo.id.toString());
  274. const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
  275. // router.push(`/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`, { scroll: false });
  276. const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
  277. if (pathname + searchParams.toString() !== newUrl) {
  278. router.replace(newUrl, { scroll: false });
  279. }
  280. },
  281. [router, selectedIdsParam, fetchPoDetail]
  282. );
  283. useEffect(() => {
  284. if (currentPoId && currentPoId !== selectedPoId.toString()) {
  285. setSelectedPoId(parseInt(currentPoId));
  286. fetchPoDetail(currentPoId);
  287. }
  288. }, [currentPoId, fetchPoDetail]);
  289. useEffect(() => {
  290. fetchPoList();
  291. }, [fetchPoList]);
  292. useEffect(() => {
  293. if (currentPoId) {
  294. setSelectedPoId(parseInt(currentPoId));
  295. }
  296. }, [currentPoId]);
  297. const removeParam = (paramToRemove: string) => {
  298. const newParams = new URLSearchParams(searchParams.toString());
  299. newParams.delete(paramToRemove);
  300. window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
  301. };
  302. const handleCompletePo = useCallback(async () => {
  303. const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
  304. console.log(checkRes);
  305. const newPo = await fetchPoInClient(purchaseOrder.id);
  306. setPurchaseOrder(newPo);
  307. }, [purchaseOrder.id]);
  308. const handleStartPo = useCallback(async () => {
  309. const startRes = await startPo(purchaseOrder.id);
  310. console.log(startRes);
  311. const newPo = await fetchPoInClient(purchaseOrder.id);
  312. setPurchaseOrder(newPo);
  313. }, [purchaseOrder.id]);
  314. const handleMailTemplateForStockInLine = useCallback(async (stockInLineId: number) => {
  315. const response = await getMailTemplatePdfForStockInLine(stockInLineId)
  316. if (response) {
  317. downloadFile(new Uint8Array(response.blobValue), response.filename);
  318. }
  319. }, [])
  320. useEffect(() => {
  321. setRows(purchaseOrder.pol || []);
  322. }, [purchaseOrder]);
  323. // useEffect(() => {
  324. // setStockInLine([])
  325. // }, []);
  326. function Row(props: { row: PurchaseOrderLine }) {
  327. const { row } = props;
  328. // const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
  329. // const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
  330. // const [open, setOpen] = useState(false);
  331. const [processedQty, setProcessedQty] = useState(row.processed);
  332. const [currStatus, setCurrStatus] = useState(row.status);
  333. // const [stockInLine, setStockInLine] = useState(row.stockInLine);
  334. const totalWeight = useMemo(
  335. () => calculateWeight(row.qty, row.uom),
  336. [row.qty, row.uom],
  337. );
  338. const weightUnit = useMemo(
  339. () => returnWeightUnit(row.uom),
  340. [row.uom],
  341. );
  342. const rowIndex = useMemo(() => {
  343. return rows.findIndex((r) => r.id === row.id)
  344. }, [])
  345. useEffect(() => {
  346. const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null
  347. if (polId) {
  348. setStockInLine(rows.find((r) => r.id == polId)!.stockInLine)
  349. }
  350. }, []);
  351. useEffect(() => {
  352. if (processedQty === row.qty) {
  353. setCurrStatus("completed".toUpperCase());
  354. } else if (processedQty > 0) {
  355. setCurrStatus("receiving".toUpperCase());
  356. } else {
  357. setCurrStatus("pending".toUpperCase());
  358. }
  359. }, [processedQty, row.qty]);
  360. const handleRowSelect = () => {
  361. // setSelectedRowId(row.id);
  362. setSelectedRow(row);
  363. setStockInLine(row.stockInLine);
  364. setProcessedQty(row.processed);
  365. };
  366. const changeStockInLines = useCallback(
  367. (id: number) => {
  368. //rows = purchaseOrderLine
  369. const target = rows.find((r) => r.id === id)
  370. const stockInLine = target!.stockInLine
  371. setStockInLine(stockInLine)
  372. setSelectedRow(target!)
  373. // console.log(pathname)
  374. // router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`);
  375. },
  376. [rows]
  377. );
  378. const handleStart = useCallback(
  379. () => {
  380. setTimeout(async () => {
  381. // post stock in line
  382. const oldId = row.id;
  383. const acceptedQty = Number(polInputList[rowIndex].dnQty);
  384. if (isNaN(acceptedQty) || acceptedQty <= 0) { alert("來貨數量必須大於0!"); return; } // Temp check, need update
  385. const postData = {
  386. dnNo: dnFormProps.watch("dnNo"),
  387. receiptDate: outputDateStringToInputDateString(dnFormProps.watch("receiptDate")),
  388. itemId: row.itemId,
  389. itemNo: row.itemNo,
  390. itemName: row.itemName,
  391. // purchaseOrderId: row.purchaseOrderId,
  392. purchaseOrderLineId: row.id,
  393. acceptedQty: acceptedQty,
  394. productLotNo: polInputList[rowIndex].lotNo || '',
  395. // acceptedQty: secondReceiveQty || 0,
  396. // acceptedQty: row.acceptedQty,
  397. };
  398. // if (secondReceiveQty === 0) return
  399. const res = await createStockInLine(postData);
  400. if (res) {
  401. fetchPoDetail(selectedPoId.toString());
  402. }
  403. console.log(res);
  404. }, 200);
  405. },
  406. [polInputList, row, dnFormProps],
  407. );
  408. const handleChange = useCallback(debounce((e: React.ChangeEvent<HTMLInputElement>) => {
  409. const raw = e.target.value;
  410. const id = e.target.id
  411. // const temp = polInputList
  412. switch (id) {
  413. case "lotNo":
  414. if (raw.trim() === '') {
  415. polInputList[rowIndex].lotNo = '';
  416. break;
  417. }
  418. polInputList[rowIndex].lotNo = raw.trim();
  419. break;
  420. case "dnQty":
  421. // Allow empty input
  422. if (raw.trim() === '') {
  423. polInputList[rowIndex].dnQty = 0;
  424. break;
  425. }
  426. // Keep digits only
  427. const cleaned = raw.replace(/[^\d]/g, '');
  428. if (cleaned === '') {
  429. // If the user typed only non-digits, keep previous value
  430. break;
  431. }
  432. // Parse and clamp to non-negative integer
  433. const next = Math.max(0, Math.floor(Number(cleaned)));
  434. polInputList[rowIndex].dnQty = next;
  435. break;
  436. default:
  437. break;
  438. }
  439. // setPolInputList(() => temp)
  440. }, 300), [rowIndex]);
  441. // const [focusField, setFocusField] = useState<HTMLInputElement>();
  442. const purchaseToStockRatio = (row.stockUom.purchaseRatioN ?? 1) / (row.stockUom.purchaseRatioD ?? 1) * (row.stockUom.stockRatioD ?? 1) / (row.stockUom.stockRatioN ?? 1)
  443. const receivedTotal = decimalFormatter.format(row.stockInLine.filter((sil) => sil.purchaseOrderLineId === row.id).reduce((acc, cur) => acc + (cur.acceptedQty ?? 0),0) * purchaseToStockRatio);
  444. const highlightColor = (Number(receivedTotal.replace(/,/g, '')) <= 0) ? "red" : "inherit";
  445. return (
  446. <>
  447. <TableRow
  448. sx={{ "& > *": { borderBottom: "unset" },
  449. color: "black"
  450. }}
  451. onClick={() => changeStockInLines(row.id)}
  452. >
  453. {/* <TableCell>
  454. <IconButton
  455. disabled={purchaseOrder.status.toLowerCase() === "pending"}
  456. aria-label="expand row"
  457. size="small"
  458. onClick={() => setOpen(!open)}
  459. >
  460. {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
  461. </IconButton>
  462. </TableCell> */}
  463. <TableCell align="center" sx={{ width: '60px' }}>
  464. <Radio
  465. checked={selectedRow?.id === row.id}
  466. // onChange={handleRowSelect}
  467. // onClick={(e) => e.stopPropagation()}
  468. />
  469. </TableCell>
  470. <TableCell align="left">{row.itemNo}</TableCell>
  471. <TableCell align="left">{row.itemName}</TableCell>
  472. <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
  473. <TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
  474. <TableCell align="left">{row.uom?.udfudesc}</TableCell>
  475. {/* <TableCell align="right">{decimalFormatter.format(row.stockUom.stockQty)}</TableCell> */}
  476. <TableCell sx={{ color: highlightColor}} align="right">{receivedTotal}</TableCell>
  477. <TableCell sx={{ color: highlightColor}} align="left">{row.stockUom.stockUomDesc}</TableCell>
  478. {/* <TableCell align="right">
  479. {decimalFormatter.format(totalWeight)} {weightUnit}
  480. </TableCell> */}
  481. {/* <TableCell align="left">{weightUnit}</TableCell> */}
  482. {/* <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell> */}
  483. {/* <TableCell align="left">{row.expiryDate}</TableCell> */}
  484. <TableCell sx={{ color: highlightColor}} align="left">{t(`${row.status.toLowerCase()}`)}</TableCell>
  485. {/* <TableCell sx={{ color: highlightColor}} align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> */}
  486. {/* <TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell> */}
  487. <TableCell align="center">
  488. <TextField
  489. id="lotNo"
  490. label="輸入貨品批號"
  491. type="text" // Use type="text" to allow validation in the change handler
  492. variant="outlined"
  493. defaultValue={polInputList[rowIndex]?.lotNo ?? ''}
  494. onChange={handleChange}
  495. // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}}
  496. />
  497. </TableCell>
  498. <TableCell align="center">
  499. <TextField
  500. id="dnQty"
  501. label="此批送貨數量"
  502. type="text" // Use type="text" to allow validation in the change handler
  503. variant="outlined"
  504. defaultValue={polInputList[rowIndex]?.dnQty ?? ''}
  505. onChange={handleChange}
  506. InputProps={{
  507. inputProps: {
  508. min: 0, // Optional: set a minimum value
  509. step: 1 // Optional: set the step for the number input
  510. }
  511. }}
  512. />
  513. </TableCell>
  514. <TableCell align="center">
  515. <Button
  516. variant="contained"
  517. onClick={() =>
  518. handleStart()
  519. }
  520. >
  521. {t("submit")}
  522. </Button>
  523. </TableCell>
  524. </TableRow>
  525. {/* <TableRow> */}
  526. {/* <TableCell /> */}
  527. {/* <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> */}
  528. {/* <Collapse in={true} timeout="auto" unmountOnExit> */}
  529. {/* <Collapse in={open} timeout="auto" unmountOnExit> */}
  530. {/* <Table>
  531. <TableBody>
  532. <TableRow>
  533. <TableCell align="right">
  534. <Box>
  535. <PoInputGrid
  536. qc={qc}
  537. setRows={setRows}
  538. stockInLine={stockInLine}
  539. setStockInLine={setStockInLine}
  540. setProcessedQty={setProcessedQty}
  541. itemDetail={row}
  542. warehouse={warehouse}
  543. />
  544. </Box>
  545. </TableCell>
  546. </TableRow>
  547. </TableBody>
  548. </Table> */}
  549. {/* </Collapse> */}
  550. {/* </TableCell> */}
  551. {/* </TableRow> */}
  552. </>
  553. );
  554. }
  555. // ROW END
  556. const [tabIndex, setTabIndex] = useState(0);
  557. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  558. (_e, newValue) => {
  559. setTabIndex(newValue);
  560. },
  561. [],
  562. );
  563. const [isOpenScanner, setOpenScanner] = useState(false);
  564. // const testing = useCallback(() => {
  565. // // setOpenScanner(true);
  566. // const newParams = new URLSearchParams(searchParams.toString());
  567. // console.log(pathname)
  568. // }, [pathname, router, searchParams]);
  569. const onOpenScanner = useCallback(() => {
  570. setOpenScanner(true);
  571. }, []);
  572. const onCloseScanner = useCallback(() => {
  573. setOpenScanner(false);
  574. }, []);
  575. const [itemInfo, setItemInfo] = useState<
  576. StockInLine & { warehouseId?: number }
  577. >();
  578. const [putAwayOpen, setPutAwayOpen] = useState(false);
  579. // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
  580. const closePutAwayModal = useCallback(() => {
  581. setPutAwayOpen(false);
  582. setItemInfo(undefined);
  583. }, []);
  584. const openPutAwayModal = useCallback(() => {
  585. setPutAwayOpen(true);
  586. }, []);
  587. const buttonData = useMemo(() => {
  588. switch (purchaseOrder.status.toLowerCase()) {
  589. case "pending":
  590. return {
  591. buttonName: "start",
  592. title: t("Do you want to start?"),
  593. confirmButtonText: t("Start"),
  594. successTitle: t("Start Success"),
  595. errorTitle: t("Start Fail"),
  596. buttonText: t("Start PO"),
  597. buttonIcon: <PlayArrow />,
  598. buttonColor: "success",
  599. disabled: false,
  600. onClick: handleStartPo,
  601. };
  602. case "receiving":
  603. return {
  604. buttonName: "complete",
  605. title: t("Do you want to complete?"),
  606. confirmButtonText: t("Complete"),
  607. successTitle: t("Complete Success"),
  608. errorTitle: t("Complete Fail"),
  609. buttonText: t("Complete PO"),
  610. buttonIcon: <DoneIcon />,
  611. buttonColor: "info",
  612. disabled: false,
  613. onClick: handleCompletePo,
  614. };
  615. default:
  616. return {
  617. buttonName: "complete",
  618. title: t("Do you want to complete?"),
  619. confirmButtonText: t("Complete"),
  620. successTitle: t("Complete Success"),
  621. errorTitle: t("Complete Fail"),
  622. buttonText: t("Complete PO"),
  623. buttonIcon: <DoneIcon />,
  624. buttonColor: "info",
  625. disabled: true,
  626. };
  627. // break;
  628. }
  629. }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
  630. const FIRST_IN_FIELD = "firstInQty"
  631. const SECOND_IN_FIELD = "secondInQty"
  632. const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
  633. switch (field) {
  634. case FIRST_IN_FIELD:
  635. return true;
  636. case SECOND_IN_FIELD:
  637. return true;
  638. default:
  639. return false; // Default case
  640. }
  641. }, []);
  642. useEffect(() => {
  643. const params = searchParams.get("polId")
  644. if (params) {
  645. const polId = parseInt(params)
  646. }
  647. }, [searchParams])
  648. const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
  649. if (value != null) {
  650. const updatedValue = dayjsToDateString(value)
  651. onChange(updatedValue)
  652. } else {
  653. onChange(value)
  654. }
  655. }, [])
  656. return (
  657. <>
  658. <Stack spacing={2}>
  659. {/* Area1: title */}
  660. <Grid container xs={12} justifyContent="start">
  661. <Grid item>
  662. <Typography mb={2} variant="h4">
  663. {purchaseOrder.code} -{" "}
  664. {t(`${purchaseOrder.status.toLowerCase()}`)}
  665. </Typography>
  666. </Grid>
  667. </Grid>
  668. {/* area2: dn info */}
  669. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
  670. {/* left side select po */}
  671. <Grid item xs={4}>
  672. <PoSearchList
  673. poList={poList}
  674. selectedPoId={selectedPoId}
  675. onSelect={handlePoSelect}
  676. />
  677. </Grid>
  678. {/* right side po info */}
  679. <Grid item xs={8}>
  680. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
  681. <Grid item xs={12}>
  682. <PoInfoCard po={purchaseOrder} />
  683. </Grid>
  684. <Grid item xs={12}>
  685. {true ? (
  686. <FormProvider {...dnFormProps}>
  687. <Stack component={"form"} spacing={2}>
  688. <Card sx={{ display: "block", height: "230px" }}>
  689. <CardContent component={Stack} spacing={2}>
  690. <Grid container spacing={2} sx={{ maxWidth: 'fit-content' }}>
  691. <Grid item xs={12}>
  692. <TextField
  693. {...dnFormProps.register("dnNo")}
  694. label={t("dnNo")}
  695. type="text"
  696. variant="outlined"
  697. fullWidth
  698. // InputProps={{
  699. // inputProps: {
  700. // min: 0,
  701. // step: 1
  702. // }
  703. // }}
  704. />
  705. </Grid>
  706. <Grid item xs={12}>
  707. <LocalizationProvider
  708. dateAdapter={AdapterDayjs}
  709. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  710. adapterLocale="zh-hk"
  711. localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
  712. >
  713. <Controller
  714. control={dnFormProps.control}
  715. name="receiptDate"
  716. render={({ field }) => (
  717. <DatePicker
  718. label={t("receiptDate")}
  719. format={`${OUTPUT_DATE_FORMAT}`}
  720. defaultValue={dateStringToDayjs(field.value)}
  721. onChange={(newValue: Dayjs | null) => {
  722. handleDatePickerChange(newValue, field.onChange)
  723. }}
  724. slotProps={{ textField: { fullWidth: true }}}
  725. />
  726. )}
  727. />
  728. </LocalizationProvider>
  729. </Grid>
  730. {/* <TextField
  731. label={t("dnDate")}
  732. type="text"
  733. variant="outlined"
  734. defaultValue={"11/08/2025"}
  735. fullWidth
  736. /> */}
  737. </Grid>
  738. {/* <Button variant="contained" onClick={onOpenScanner} fullWidth>
  739. 提交
  740. </Button> */}
  741. </CardContent>
  742. </Card>
  743. </Stack>
  744. </FormProvider>
  745. ) : undefined}
  746. </Grid>
  747. </Grid>
  748. </Grid>
  749. </Grid>
  750. {/* Area4: Main Table */}
  751. <Grid container xs={12} justifyContent="start">
  752. <Grid item xs={12}>
  753. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  754. <Table aria-label="collapsible table" stickyHeader>
  755. <TableHead>
  756. <TableRow>
  757. <TableCell align="center" sx={{ width: '60px' }}></TableCell>
  758. <TableCell sx={{ width: '125px' }}>{t("itemNo")}</TableCell>
  759. <TableCell align="left" sx={{ width: '125px' }}>{t("itemName")}</TableCell>
  760. <TableCell align="right">{t("qty")}</TableCell>
  761. <TableCell align="right">{t("processedQty")}</TableCell>
  762. <TableCell align="left">{t("uom")}</TableCell>
  763. <TableCell align="right">{t("receivedTotal")}</TableCell>
  764. <TableCell align="left">{t("Stock UoM")}</TableCell>
  765. {/* <TableCell align="right">{t("total weight")}</TableCell> */}
  766. {/* <TableCell align="right">{`${t("price")} (HKD)`}</TableCell> */}
  767. <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
  768. {/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} */}
  769. <TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
  770. {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}<br/>(以訂單單位計算)</TableCell> : undefined}
  771. <TableCell align="center" sx={{ width: '100px' }}></TableCell>
  772. </TableRow>
  773. </TableHead>
  774. <TableBody>
  775. {rows.map((row) => (
  776. <Row key={row.id} row={row} />
  777. ))}
  778. </TableBody>
  779. </Table>
  780. </TableContainer>
  781. </Grid>
  782. </Grid>
  783. {/* area5: selected item info */}
  784. <Grid container xs={12} justifyContent="start">
  785. <Grid item xs={12}>
  786. <Typography variant="h6">
  787. {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"}
  788. </Typography>
  789. </Grid>
  790. <Grid item xs={12}>
  791. {selectedRow && (
  792. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  793. <Table>
  794. <TableBody>
  795. <TableRow>
  796. <TableCell align="right">
  797. <Box>
  798. <PoInputGrid
  799. // qc={qc}
  800. setRows={setRows}
  801. stockInLine={stockInLine}
  802. setStockInLine={setStockInLine}
  803. setProcessedQty={setProcessedQty}
  804. itemDetail={selectedRow}
  805. warehouse={warehouse}
  806. fetchPoDetail={fetchPoDetail}
  807. handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
  808. printerCombo={printerCombo}
  809. />
  810. </Box>
  811. </TableCell>
  812. </TableRow>
  813. </TableBody>
  814. </Table>
  815. </TableContainer>
  816. )}
  817. </Grid>
  818. </Grid>
  819. {/* tab 2 */}
  820. <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
  821. {/* <StyledDataGrid
  822. /> */}
  823. </Grid>
  824. </Stack>
  825. {/* {itemInfo !== undefined && (
  826. <>
  827. <PoQcStockInModal
  828. type={"putaway"}
  829. open={putAwayOpen}
  830. warehouse={warehouse}
  831. setItemDetail={setItemInfo}
  832. onClose={closePutAwayModal}
  833. itemDetail={itemInfo}
  834. />
  835. </>
  836. )} */}
  837. </>
  838. );
  839. };
  840. export default PoDetail;