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

PoDetail.tsx 49 KiB

11ヶ月前
1ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
9ヶ月前
8ヶ月前
8ヶ月前
8ヶ月前
9ヶ月前
5ヶ月前
8ヶ月前
8ヶ月前
8ヶ月前
8ヶ月前
2週間前
11ヶ月前
8ヶ月前
2週間前
2週間前
8ヶ月前
2週間前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
8ヶ月前
2週間前
8ヶ月前
5ヶ月前
5ヶ月前
9ヶ月前
11ヶ月前
9ヶ月前
9ヶ月前
11ヶ月前
2週間前
9ヶ月前
9ヶ月前
11ヶ月前
1ヶ月前
11ヶ月前
1ヶ月前
11ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
9ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2週間前
1ヶ月前
9ヶ月前
2週間前
8ヶ月前
8ヶ月前
2週間前
8ヶ月前
6ヶ月前
9ヶ月前
11ヶ月前
6ヶ月前
8ヶ月前
9ヶ月前
2週間前
2週間前
8ヶ月前
8ヶ月前
9ヶ月前
2週間前
2週間前
9ヶ月前
1ヶ月前
9ヶ月前
9ヶ月前
2週間前
9ヶ月前
11ヶ月前
9ヶ月前
8ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
11ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
9ヶ月前
11ヶ月前
11ヶ月前
9ヶ月前
9ヶ月前
8ヶ月前
9ヶ月前
8ヶ月前
8ヶ月前
8ヶ月前
9ヶ月前
8ヶ月前
8ヶ月前
11ヶ月前
11ヶ月前
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328
  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. alpha,
  33. Autocomplete,
  34. Dialog,
  35. DialogActions,
  36. DialogContent,
  37. DialogTitle,
  38. } from "@mui/material";
  39. import { useTranslation } from "react-i18next";
  40. import { submitDialogWithWarning } from "../Swal/CustomAlerts";
  41. // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
  42. import {
  43. GridColDef,
  44. GridRowId,
  45. GridRowModel,
  46. useGridApiRef,
  47. } from "@mui/x-data-grid";
  48. import {
  49. checkPolAndCompletePo,
  50. fetchPoInClient,
  51. fetchPoSummariesClient,
  52. startPo,
  53. } from "@/app/api/po/actions";
  54. import {
  55. createStockInLine
  56. } from "@/app/api/stockIn/actions";
  57. import {
  58. useCallback,
  59. useContext,
  60. useEffect,
  61. useMemo,
  62. useState,
  63. } from "react";
  64. import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
  65. import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
  66. import PoInputGrid from "./PoInputGrid";
  67. // import { QcItemWithChecks } from "@/app/api/qc";
  68. import { useRouter, useSearchParams, usePathname } from "next/navigation";
  69. import { WarehouseResult } from "@/app/api/warehouse";
  70. import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
  71. import { CameraContext } from "../Cameras/CameraProvider";
  72. import QrModal from "./QrModal";
  73. import { PlayArrow } from "@mui/icons-material";
  74. import DoneIcon from "@mui/icons-material/Done";
  75. import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
  76. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  77. import { arrayToDateString } from "@/app/utils/formatUtil";
  78. import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
  79. import { Controller, FormProvider, useForm } from "react-hook-form";
  80. import dayjs, { Dayjs } from "dayjs";
  81. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  82. import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
  83. import LoadingComponent from "../General/LoadingComponent";
  84. import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions";
  85. import { PrinterCombo } from "@/app/api/settings/printer";
  86. import { EscalationCombo } from "@/app/api/user";
  87. import { StockInLine } from "@/app/api/stockIn";
  88. import { printQrCodeForSil } from "@/app/api/stockIn/actions";
  89. import { useSession } from "next-auth/react";
  90. import { AUTH } from "@/authorities";
  91. //import { useRouter } from "next/navigation";
  92. type Props = {
  93. po: PoResult;
  94. // qc: QcItemWithChecks[];
  95. warehouse: WarehouseResult[];
  96. printerCombo: PrinterCombo[];
  97. };
  98. /** PO stock-in lines still in pre-complete workflow (align with nav alert: pending / receiving). */
  99. const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]);
  100. /** Sum of put-away in stock units (matches StockInForm「已上架數量」stockQty). */
  101. function totalPutAwayStockQtyForPol(row: PurchaseOrderLine): number {
  102. return row.stockInLine
  103. .filter((sil) => sil.purchaseOrderLineId === row.id)
  104. .reduce((acc, sil) => {
  105. const lineSum =
  106. sil.putAwayLines?.reduce(
  107. (s, p) => s + Number(p.stockQty ?? p.qty ?? 0),
  108. 0,
  109. ) ?? 0;
  110. return acc + lineSum;
  111. }, 0);
  112. }
  113. /** POL order demand in stock units (same basis as PoDetail processed / backend PO detail). */
  114. function polOrderStockQty(row: PurchaseOrderLine): number {
  115. return Number(row.stockUom?.stockQty ?? row.qty ?? 0);
  116. }
  117. function purchaseOrderLineHasIncompleteStockIn(row: PurchaseOrderLine): boolean {
  118. const orderStock = polOrderStockQty(row);
  119. const putAway = totalPutAwayStockQtyForPol(row);
  120. if (orderStock > 0 && putAway >= orderStock) {
  121. return false;
  122. }
  123. return row.stockInLine
  124. .filter((sil) => sil.purchaseOrderLineId === row.id)
  125. .some((sil) =>
  126. PURCHASE_STOCK_IN_ALERT_STATUSES.has((sil.status ?? "").toLowerCase().trim()),
  127. );
  128. }
  129. type EntryError =
  130. | {
  131. [field in keyof StockInLine]?: string;
  132. }
  133. | undefined;
  134. // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
  135. const PoSearchList: React.FC<{
  136. poList: PoResult[];
  137. selectedPoId: number;
  138. onSelect: (po: PoResult) => void;
  139. loading?: boolean;
  140. }> = ({ poList, selectedPoId, onSelect, loading = false }) => {
  141. const { t } = useTranslation(["purchaseOrder", "dashboard"]);
  142. const [searchTerm, setSearchTerm] = useState('');
  143. const filteredPoList = useMemo(() => {
  144. if (searchTerm.trim() === '') {
  145. return poList;
  146. }
  147. return poList.filter(poItem =>
  148. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  149. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  150. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  151. );
  152. }, [poList, searchTerm, t]);
  153. return (
  154. <Paper
  155. sx={{
  156. p: 2,
  157. minWidth: "300px",
  158. height: "100%",
  159. display: "flex",
  160. flexDirection: "column",
  161. overflow: "hidden",
  162. }}
  163. >
  164. <Typography variant="h6" gutterBottom>
  165. {t("Purchase Order")}
  166. </Typography>
  167. <TextField
  168. label={t("Search")}
  169. variant="outlined"
  170. size="small"
  171. fullWidth
  172. value={searchTerm}
  173. onChange={(e) => setSearchTerm(e.target.value)}
  174. sx={{ mb: 2 }}
  175. InputProps={{
  176. startAdornment: (
  177. <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
  178. </Typography>
  179. ),
  180. }}
  181. />
  182. <Box sx={{ flex: 1, overflow: "auto" }}>
  183. {loading ? (
  184. <LoadingComponent />
  185. ) : filteredPoList.length > 0 ? (
  186. <List dense sx={{ width: "100%" }}>
  187. {filteredPoList.map((poItem, index) => (
  188. <div key={poItem.id}>
  189. <ListItem disablePadding sx={{ width: "100%" }}>
  190. <ListItemButton
  191. selected={selectedPoId === poItem.id}
  192. onClick={() => onSelect(poItem)}
  193. sx={{
  194. width: "100%",
  195. "&.Mui-selected": {
  196. backgroundColor: "primary.light",
  197. "&:hover": {
  198. backgroundColor: "primary.light",
  199. },
  200. },
  201. }}
  202. >
  203. <ListItemText
  204. primary={
  205. <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
  206. {poItem.code}
  207. </Typography>
  208. }
  209. secondary={
  210. <Typography variant="caption" color="text.secondary">
  211. {t(`${poItem.status.toLowerCase()}`)}
  212. </Typography>
  213. }
  214. />
  215. </ListItemButton>
  216. </ListItem>
  217. {index < filteredPoList.length - 1 && <Divider />}
  218. </div>
  219. ))}
  220. </List>
  221. ) : (
  222. <Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
  223. {searchTerm.trim()
  224. ? t("No purchase orders match your search", { defaultValue: "沒有符合搜尋的採購單" })
  225. : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
  226. </Typography>
  227. )}
  228. </Box>
  229. {searchTerm && (
  230. <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
  231. {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
  232. {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */}
  233. </Typography>
  234. )}
  235. </Paper>
  236. );
  237. };
  238. interface PolInputResult {
  239. lotNo: string,
  240. dnQty: string,
  241. }
  242. const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
  243. const cameras = useContext(CameraContext);
  244. const { data: session } = useSession();
  245. const canSeeStockInReminders = useMemo(() => {
  246. const set = new Set((session?.user?.abilities ?? []).map((a) => String(a).trim()));
  247. return set.has(AUTH.TESTING) || set.has(AUTH.ADMIN) || set.has(AUTH.STOCK);
  248. }, [session?.user?.abilities]);
  249. // console.log(cameras);
  250. const { t } = useTranslation("purchaseOrder");
  251. const apiRef = useGridApiRef();
  252. const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
  253. const [rows, setRows] = useState<PurchaseOrderLine[]>(
  254. purchaseOrder.pol || [],
  255. );
  256. const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({})
  257. const PO_DETAIL_SELECTION_KEY = "po-detail-selection";
  258. useEffect(() => {
  259. setPolInputList((prev) => {
  260. const next: Record<number, PolInputResult> = {};
  261. (purchaseOrder.pol ?? []).forEach((pol) => {
  262. next[pol.id] = prev[pol.id] ?? {
  263. lotNo: "",
  264. dnQty: "",
  265. };
  266. });
  267. return next;
  268. });
  269. }, [purchaseOrder.pol]);
  270. useEffect(() => {
  271. try {
  272. const raw = sessionStorage.getItem("po-detail-selection");
  273. if (raw) {
  274. const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[];
  275. if (Array.isArray(parsed) && parsed.length > 0) {
  276. setPoList(parsed as PoResult[]);
  277. sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据
  278. }
  279. }
  280. } catch (e) {
  281. console.warn("sessionStorage getItem/parse failed", e);
  282. }
  283. }, []);
  284. const pathname = usePathname()
  285. const searchParams = useSearchParams();
  286. const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);
  287. const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
  288. const [processedQty, setProcessedQty] = useState(0);
  289. useEffect(() => {
  290. const polIdParam = searchParams.get("polId");
  291. if (!polIdParam || rows.length === 0) return;
  292. const match = rows.find((r) => r.id.toString() === polIdParam);
  293. if (match) {
  294. setSelectedRow(match);
  295. setStockInLine(match.stockInLine);
  296. setProcessedQty(match.processed);
  297. }
  298. }, [rows, searchParams]);
  299. const router = useRouter();
  300. const [poList, setPoList] = useState<PoResult[]>(() => [po]);
  301. const [isPoListLoading, setIsPoListLoading] = useState(false);
  302. const [selectedPoId, setSelectedPoId] = useState(po.id);
  303. const [focusField, setFocusField] = useState<HTMLInputElement>();
  304. const currentPoId = searchParams.get('id');
  305. const selectedIdsParam = searchParams.get('selectedIds');
  306. // const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  307. const dnFormProps = useForm({
  308. defaultValues: {
  309. dnNo: '',
  310. receiptDate: dayjsToDateString(dayjs())
  311. }
  312. })
  313. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
  314. printerCombo?.[0],
  315. );
  316. const [printQty, setPrintQty] = useState(1);
  317. const [printDialogOpen, setPrintDialogOpen] = useState(false);
  318. const [isBulkPrinting, setIsBulkPrinting] = useState(false);
  319. const [printStatusFilter, setPrintStatusFilter] = useState({
  320. received: true,
  321. completed: false,
  322. });
  323. const [selectedPrintSilIds, setSelectedPrintSilIds] = useState<Set<number>>(
  324. () => new Set(),
  325. );
  326. const eligiblePrintSils = useMemo(() => {
  327. const statusSet = new Set<string>();
  328. if (printStatusFilter.received) statusSet.add("received");
  329. if (printStatusFilter.completed) statusSet.add("completed");
  330. const pols = purchaseOrder.pol ?? [];
  331. return pols
  332. .flatMap((pol) => pol.stockInLine ?? [])
  333. .filter((sil) => statusSet.has((sil.status ?? "").toLowerCase().trim()));
  334. }, [purchaseOrder.pol, printStatusFilter.completed, printStatusFilter.received]);
  335. const openPrintDialog = useCallback(() => {
  336. setSelectedPrintSilIds(new Set());
  337. setPrintDialogOpen(true);
  338. }, []);
  339. const closePrintDialog = useCallback(() => {
  340. if (isBulkPrinting) return;
  341. setPrintDialogOpen(false);
  342. }, [isBulkPrinting]);
  343. const togglePrintSilSelection = useCallback((id: number, checked: boolean) => {
  344. setSelectedPrintSilIds((prev) => {
  345. const next = new Set(prev);
  346. if (checked) next.add(id);
  347. else next.delete(id);
  348. return next;
  349. });
  350. }, []);
  351. const setAllVisiblePrintSilsSelected = useCallback((checked: boolean) => {
  352. setSelectedPrintSilIds(() => {
  353. if (!checked) return new Set();
  354. return new Set(eligiblePrintSils.map((s) => s.id));
  355. });
  356. }, [eligiblePrintSils]);
  357. const handleBulkPrint = useCallback(async () => {
  358. if (!selectedPrinter) {
  359. alert("請先選擇印表機");
  360. return;
  361. }
  362. if (!Number.isFinite(printQty) || printQty <= 0) {
  363. alert("列印數量必須大於 0");
  364. return;
  365. }
  366. const ids = Array.from(selectedPrintSilIds.values());
  367. if (ids.length <= 0) {
  368. alert("請先選擇要列印的項目");
  369. return;
  370. }
  371. setIsBulkPrinting(true);
  372. try {
  373. for (const id of ids) {
  374. await printQrCodeForSil({
  375. stockInLineId: id,
  376. printerId: selectedPrinter.id,
  377. printQty,
  378. });
  379. }
  380. setPrintDialogOpen(false);
  381. } finally {
  382. setIsBulkPrinting(false);
  383. }
  384. }, [printQty, selectedPrinter, selectedPrintSilIds]);
  385. /** Only loads sidebar list when `selectedIds` is in the URL; otherwise show current PO only (no /po/list fetch). */
  386. const fetchPoList = useCallback(async () => {
  387. if (!selectedIdsParam) return;
  388. setIsPoListLoading(true);
  389. try {
  390. const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死
  391. const allIds = selectedIdsParam
  392. .split(',')
  393. .map((id) => parseInt(id))
  394. .filter((id) => !Number.isNaN(id));
  395. const limitedIds = allIds.slice(0, MAX_IDS);
  396. if (allIds.length > MAX_IDS) {
  397. console.warn(`selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.`);
  398. }
  399. const result = await fetchPoSummariesClient(limitedIds);
  400. setPoList(result as any);
  401. } catch (error) {
  402. console.error("Failed to fetch PO list:", error);
  403. } finally {
  404. setIsPoListLoading(false);
  405. }
  406. }, [selectedIdsParam]);
  407. const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => {
  408. try {
  409. const result = await fetchPoInClient(parseInt(poId));
  410. if (result) {
  411. console.log("%c Fetched PO:", "color:orange", result);
  412. setPurchaseOrder(result);
  413. const currentDnNo = preserveDnNo ? dnFormProps.getValues("dnNo") : "";
  414. dnFormProps.reset({
  415. dnNo: currentDnNo,
  416. receiptDate: dayjsToDateString(dayjs()),
  417. });
  418. setRows(result.pol || []);
  419. if (result.pol && result.pol.length > 0) {
  420. const targetPolId = preferredPolId ?? selectedRow?.id;
  421. const targetPol =
  422. result.pol.find((p) => p.id === targetPolId) ?? result.pol[0];
  423. setSelectedRow(targetPol);
  424. setStockInLine(targetPol.stockInLine);
  425. setProcessedQty(targetPol.processed);
  426. }
  427. // if (focusField) {console.log(focusField);focusField.focus();}
  428. }
  429. } catch (error) {
  430. console.error("Failed to fetch PO detail:", error);
  431. }
  432. }, [selectedRow, selectedPoId]);
  433. const handlePoSelect = useCallback(
  434. async (selectedPo: PoResult) => {
  435. if (selectedPo.id === selectedPoId) return;
  436. setSelectedPoId(selectedPo.id);
  437. await fetchPoDetail(selectedPo.id.toString());
  438. const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
  439. const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
  440. if (pathname + searchParams.toString() !== newUrl) {
  441. router.replace(newUrl, { scroll: false });
  442. }
  443. },
  444. [selectedPoId, fetchPoDetail, selectedIdsParam, pathname, searchParams, router]
  445. );
  446. useEffect(() => {
  447. if (currentPoId && currentPoId !== selectedPoId.toString()) {
  448. setSelectedPoId(parseInt(currentPoId));
  449. fetchPoDetail(currentPoId);
  450. }
  451. }, [currentPoId, fetchPoDetail]);
  452. useEffect(() => {
  453. if (selectedIdsParam) {
  454. void fetchPoList();
  455. }
  456. }, [selectedIdsParam, fetchPoList]);
  457. useEffect(() => {
  458. if (selectedIdsParam) return;
  459. setPoList([purchaseOrder]);
  460. }, [selectedIdsParam, purchaseOrder]);
  461. useEffect(() => {
  462. if (currentPoId) {
  463. setSelectedPoId(parseInt(currentPoId));
  464. }
  465. }, [currentPoId]);
  466. const removeParam = (paramToRemove: string) => {
  467. const newParams = new URLSearchParams(searchParams.toString());
  468. newParams.delete(paramToRemove);
  469. window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
  470. };
  471. const handleCompletePo = useCallback(async () => {
  472. const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
  473. console.log(checkRes);
  474. const newPo = await fetchPoInClient(purchaseOrder.id);
  475. setPurchaseOrder(newPo);
  476. }, [purchaseOrder.id]);
  477. const handleStartPo = useCallback(async () => {
  478. const startRes = await startPo(purchaseOrder.id);
  479. console.log(startRes);
  480. const newPo = await fetchPoInClient(purchaseOrder.id);
  481. setPurchaseOrder(newPo);
  482. }, [purchaseOrder.id]);
  483. const handleMailTemplateForStockInLine = useCallback(async (stockInLineId: number) => {
  484. const response = await getMailTemplatePdfForStockInLine(stockInLineId)
  485. if (response) {
  486. downloadFile(new Uint8Array(response.blobValue), response.filename);
  487. }
  488. }, [])
  489. useEffect(() => {
  490. setRows(purchaseOrder.pol || []);
  491. }, [purchaseOrder]);
  492. // useEffect(() => {
  493. // setStockInLine([])
  494. // }, []);
  495. function Row(props: { row: PurchaseOrderLine }) {
  496. const { row } = props;
  497. // const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
  498. // const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
  499. // const [open, setOpen] = useState(false);
  500. const [processedQty, setProcessedQty] = useState(row.processed);
  501. const [currStatus, setCurrStatus] = useState(row.status);
  502. const [lotNoInput, setLotNoInput] = useState(polInputList[row.id]?.lotNo ?? "");
  503. const [dnQtyInput, setDnQtyInput] = useState(polInputList[row.id]?.dnQty ?? "");
  504. // const [stockInLine, setStockInLine] = useState(row.stockInLine);
  505. const totalWeight = useMemo(
  506. () => calculateWeight(row.qty, row.uom),
  507. [row.qty, row.uom],
  508. );
  509. const weightUnit = useMemo(
  510. () => returnWeightUnit(row.uom),
  511. [row.uom],
  512. );
  513. useEffect(() => {
  514. const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null
  515. if (polId) {
  516. setStockInLine(rows.find((r) => r.id == polId)!.stockInLine)
  517. }
  518. }, []);
  519. useEffect(() => {
  520. // `processedQty` comes from putAwayLines (stock unit).
  521. // After the fix, `row.qty` is qtyM18 (M18 unit), so compare using stockUom demand.
  522. const targetStockQty = Number(row.stockUom?.stockQty ?? row.qty ?? 0);
  523. if (targetStockQty > 0 && processedQty >= targetStockQty) {
  524. setCurrStatus("completed".toUpperCase());
  525. } else if (processedQty > 0) {
  526. setCurrStatus("receiving".toUpperCase());
  527. } else {
  528. setCurrStatus("pending".toUpperCase());
  529. }
  530. }, [processedQty, row.qty, row.stockUom?.stockQty]);
  531. useEffect(() => {
  532. setLotNoInput(polInputList[row.id]?.lotNo ?? "");
  533. setDnQtyInput(polInputList[row.id]?.dnQty ?? "");
  534. }, [polInputList, row.id]);
  535. const handleRowSelect = () => {
  536. // setSelectedRowId(row.id);
  537. setSelectedRow(row);
  538. setStockInLine(row.stockInLine);
  539. setProcessedQty(row.processed);
  540. };
  541. const changeStockInLines = useCallback(
  542. (id: number) => {
  543. //rows = purchaseOrderLine
  544. const target = rows.find((r) => r.id === id)
  545. const stockInLine = target!.stockInLine
  546. setStockInLine(stockInLine)
  547. setSelectedRow(target!)
  548. // console.log(pathname)
  549. // router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`);
  550. },
  551. [rows]
  552. );
  553. const handleStart = useCallback(
  554. () => {
  555. const orderQty = Number(row?.qty) ?? 0;
  556. const acceptedQty = Number(dnQtyInput.trim());
  557. if (isNaN(acceptedQty) || acceptedQty <= 0) {
  558. alert("來貨數量必須大於0!");
  559. return;
  560. }
  561. const doSubmit = () => {
  562. setTimeout(async () => {
  563. const currentDnNo = dnFormProps.watch("dnNo");
  564. const postData = {
  565. dnNo: dnFormProps.watch("dnNo"),
  566. receiptDate: outputDateStringToInputDateString(dnFormProps.watch("receiptDate")),
  567. itemId: row.itemId,
  568. itemNo: row.itemNo,
  569. itemName: row.itemName,
  570. purchaseOrderLineId: row.id,
  571. acceptedQty: acceptedQty,
  572. productLotNo: lotNoInput || "",
  573. };
  574. const res = await createStockInLine(postData);
  575. if (res) {
  576. setLotNoInput("");
  577. setDnQtyInput("");
  578. setPolInputList((prev) => ({
  579. ...prev,
  580. [row.id]: { lotNo: "", dnQty: "" },
  581. }));
  582. setSelectedRow(row);
  583. fetchPoDetail(selectedPoId.toString(), true, row.id);
  584. }
  585. console.log(res);
  586. }, 200);
  587. };
  588. const exceedOrderBy10Percent = orderQty > 0 && acceptedQty > orderQty * 1.1;
  589. if (exceedOrderBy10Percent) {
  590. submitDialogWithWarning(doSubmit, t, {
  591. title: t("Confirm submit"),
  592. html: t("This batch quantity exceeds order quantity. Do you still want to submit?"),
  593. confirmButtonText: t("Submit"),
  594. });
  595. } else {
  596. doSubmit();
  597. }
  598. },
  599. [dnQtyInput, row, dnFormProps, selectedPoId, fetchPoDetail, t, lotNoInput],
  600. );
  601. const syncRowInputToParent = useCallback((lotNo: string, dnQty: string) => {
  602. setPolInputList((prev) => {
  603. const current = prev[row.id] ?? { lotNo: "", dnQty: "" };
  604. if (current.lotNo === lotNo && current.dnQty === dnQty) return prev;
  605. return {
  606. ...prev,
  607. [row.id]: { lotNo, dnQty },
  608. };
  609. });
  610. }, [row.id]);
  611. // const [focusField, setFocusField] = useState<HTMLInputElement>();
  612. // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty
  613. const batchPurchaseQty = Number(dnQtyInput.trim()) || 0;
  614. // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計
  615. const totalStockReceived = row.stockInLine
  616. .filter((sil) => sil.purchaseOrderLineId === row.id)
  617. .reduce((acc, cur) => acc + (cur.acceptedQty ?? 0), 0);
  618. const receivedTotalText = decimalFormatter.format(totalStockReceived);
  619. const highlightColor =
  620. Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit";
  621. const needsStockInAttention =
  622. canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row);
  623. return (
  624. <>
  625. <TableRow
  626. hover
  627. title={
  628. needsStockInAttention
  629. ? "採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。"
  630. : undefined
  631. }
  632. sx={{
  633. "& > *": { borderBottom: "unset" },
  634. color: "black",
  635. ...(needsStockInAttention
  636. ? (theme) => ({
  637. boxShadow: `inset 4px 0 0 ${theme.palette.error.main}`,
  638. backgroundColor: alpha(theme.palette.error.main, 0.07),
  639. })
  640. : {}),
  641. }}
  642. onClick={() => changeStockInLines(row.id)}
  643. >
  644. {/* <TableCell>
  645. <IconButton
  646. disabled={purchaseOrder.status.toLowerCase() === "pending"}
  647. aria-label="expand row"
  648. size="small"
  649. onClick={() => setOpen(!open)}
  650. >
  651. {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
  652. </IconButton>
  653. </TableCell> */}
  654. <TableCell align="center" sx={{ width: "60px", position: "relative" }}>
  655. {needsStockInAttention && (
  656. <Box
  657. component="span"
  658. aria-hidden
  659. sx={{
  660. position: "absolute",
  661. top: 6,
  662. left: 8,
  663. width: 10,
  664. height: 10,
  665. borderRadius: "50%",
  666. bgcolor: "error.main",
  667. border: "2px solid",
  668. borderColor: "background.paper",
  669. boxShadow: (theme) => `0 0 0 1px ${alpha(theme.palette.error.main, 0.45)}`,
  670. zIndex: 1,
  671. }}
  672. />
  673. )}
  674. <Radio
  675. checked={selectedRow?.id === row.id}
  676. // onChange={handleRowSelect}
  677. // onClick={(e) => e.stopPropagation()}
  678. />
  679. </TableCell>
  680. <TableCell align="left">{row.itemNo}</TableCell>
  681. <TableCell align="left">{row.itemName}</TableCell>
  682. <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
  683. <TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
  684. <TableCell align="left">{row.uom?.udfudesc}</TableCell>
  685. {/* <TableCell align="right">{decimalFormatter.format(row.stockUom.stockQty)}</TableCell> */}
  686. {/* <TableCell sx={{ color: highlightColor}} align="right">{receivedTotal}</TableCell> */}
  687. <TableCell sx={{ color: highlightColor }} align="right">
  688. {decimalFormatter.format(totalStockReceived)}
  689. </TableCell>
  690. <TableCell sx={{ color: highlightColor}} align="left">{row.stockUom.stockUomDesc}</TableCell>
  691. {/* <TableCell align="right">
  692. {decimalFormatter.format(totalWeight)} {weightUnit}
  693. </TableCell> */}
  694. {/* <TableCell align="left">{weightUnit}</TableCell> */}
  695. {/* <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell> */}
  696. {/* <TableCell align="left">{row.expiryDate}</TableCell> */}
  697. <TableCell sx={{ color: highlightColor}} align="left">{t(`${row.status.toLowerCase()}`)}</TableCell>
  698. {/* <TableCell sx={{ color: highlightColor}} align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> */}
  699. {/* <TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell> */}
  700. <TableCell align="center">
  701. <TextField
  702. id="lotNo"
  703. label="輸入貨品批號"
  704. type="text" // Use type="text" to allow validation in the change handler
  705. variant="outlined"
  706. value={lotNoInput}
  707. onChange={(e) => setLotNoInput(e.target.value)}
  708. onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
  709. onClick={(e) => e.stopPropagation()}
  710. // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}}
  711. />
  712. </TableCell>
  713. <TableCell align="center">
  714. <TextField
  715. id="dnQty"
  716. label="此批送貨數量"
  717. type="text" // Use type="text" to allow validation in the change handler
  718. variant="outlined"
  719. value={dnQtyInput}
  720. onChange={(e) => setDnQtyInput(e.target.value)}
  721. onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
  722. onClick={(e) => e.stopPropagation()}
  723. InputProps={{
  724. inputProps: {
  725. min: 0, // Optional: set a minimum value
  726. step: "any",
  727. inputMode: "decimal",
  728. }
  729. }}
  730. />
  731. </TableCell>
  732. <TableCell align="center">
  733. <Button
  734. variant="contained"
  735. onClick={(e) => {
  736. e.stopPropagation();
  737. handleStart();
  738. }}
  739. >
  740. {t("submit")}
  741. </Button>
  742. </TableCell>
  743. </TableRow>
  744. {/* <TableRow> */}
  745. {/* <TableCell /> */}
  746. {/* <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> */}
  747. {/* <Collapse in={true} timeout="auto" unmountOnExit> */}
  748. {/* <Collapse in={open} timeout="auto" unmountOnExit> */}
  749. {/* <Table>
  750. <TableBody>
  751. <TableRow>
  752. <TableCell align="right">
  753. <Box>
  754. <PoInputGrid
  755. qc={qc}
  756. setRows={setRows}
  757. stockInLine={stockInLine}
  758. setStockInLine={setStockInLine}
  759. setProcessedQty={setProcessedQty}
  760. itemDetail={row}
  761. warehouse={warehouse}
  762. />
  763. </Box>
  764. </TableCell>
  765. </TableRow>
  766. </TableBody>
  767. </Table> */}
  768. {/* </Collapse> */}
  769. {/* </TableCell> */}
  770. {/* </TableRow> */}
  771. </>
  772. );
  773. }
  774. // ROW END
  775. const [tabIndex, setTabIndex] = useState(0);
  776. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  777. (_e, newValue) => {
  778. setTabIndex(newValue);
  779. },
  780. [],
  781. );
  782. const [isOpenScanner, setOpenScanner] = useState(false);
  783. // const testing = useCallback(() => {
  784. // // setOpenScanner(true);
  785. // const newParams = new URLSearchParams(searchParams.toString());
  786. // console.log(pathname)
  787. // }, [pathname, router, searchParams]);
  788. const onOpenScanner = useCallback(() => {
  789. setOpenScanner(true);
  790. }, []);
  791. const onCloseScanner = useCallback(() => {
  792. setOpenScanner(false);
  793. }, []);
  794. const [itemInfo, setItemInfo] = useState<
  795. StockInLine & { warehouseId?: number }
  796. >();
  797. const [putAwayOpen, setPutAwayOpen] = useState(false);
  798. // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
  799. const closePutAwayModal = useCallback(() => {
  800. setPutAwayOpen(false);
  801. setItemInfo(undefined);
  802. }, []);
  803. const openPutAwayModal = useCallback(() => {
  804. setPutAwayOpen(true);
  805. }, []);
  806. const buttonData = useMemo(() => {
  807. switch (purchaseOrder.status.toLowerCase()) {
  808. case "pending":
  809. return {
  810. buttonName: "start",
  811. title: t("Do you want to start?"),
  812. confirmButtonText: t("Start"),
  813. successTitle: t("Start Success"),
  814. errorTitle: t("Start Fail"),
  815. buttonText: t("Start PO"),
  816. buttonIcon: <PlayArrow />,
  817. buttonColor: "success",
  818. disabled: false,
  819. onClick: handleStartPo,
  820. };
  821. case "receiving":
  822. return {
  823. buttonName: "complete",
  824. title: t("Do you want to complete?"),
  825. confirmButtonText: t("Complete"),
  826. successTitle: t("Complete Success"),
  827. errorTitle: t("Complete Fail"),
  828. buttonText: t("Complete PO"),
  829. buttonIcon: <DoneIcon />,
  830. buttonColor: "info",
  831. disabled: false,
  832. onClick: handleCompletePo,
  833. };
  834. default:
  835. return {
  836. buttonName: "complete",
  837. title: t("Do you want to complete?"),
  838. confirmButtonText: t("Complete"),
  839. successTitle: t("Complete Success"),
  840. errorTitle: t("Complete Fail"),
  841. buttonText: t("Complete PO"),
  842. buttonIcon: <DoneIcon />,
  843. buttonColor: "info",
  844. disabled: true,
  845. };
  846. // break;
  847. }
  848. }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
  849. const FIRST_IN_FIELD = "firstInQty"
  850. const SECOND_IN_FIELD = "secondInQty"
  851. const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
  852. switch (field) {
  853. case FIRST_IN_FIELD:
  854. return true;
  855. case SECOND_IN_FIELD:
  856. return true;
  857. default:
  858. return false; // Default case
  859. }
  860. }, []);
  861. const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
  862. if (value != null) {
  863. const updatedValue = dayjsToDateString(value)
  864. onChange(updatedValue)
  865. } else {
  866. onChange(value)
  867. }
  868. }, [])
  869. const fillTodayLotNo = useCallback(() => {
  870. const today = dayjs().format("YYYYMMDD");
  871. setPolInputList((prev) => {
  872. const next: Record<number, PolInputResult> = { ...prev };
  873. (rows ?? []).forEach((r) => {
  874. const current = next[r.id] ?? { lotNo: "", dnQty: "" };
  875. const lotNo = (current.lotNo ?? "").trim();
  876. if (!lotNo) {
  877. next[r.id] = { ...current, lotNo: today };
  878. }
  879. });
  880. return next;
  881. });
  882. }, [rows]);
  883. return (
  884. <>
  885. <Stack spacing={2}>
  886. {/* Area1: title */}
  887. <Grid container xs={12} justifyContent="start">
  888. <Grid item>
  889. <Typography mb={2} variant="h4">
  890. {purchaseOrder.code} -{" "}
  891. {t(`${purchaseOrder.status.toLowerCase()}`)}
  892. </Typography>
  893. </Grid>
  894. </Grid>
  895. {/* area2: dn info */}
  896. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }} alignItems="stretch">
  897. {/* left side select po */}
  898. <Grid item xs={4} sx={{ display: "flex" }}>
  899. <Stack spacing={1} sx={{ flex: 1 }}>
  900. <PoSearchList
  901. poList={poList}
  902. selectedPoId={selectedPoId}
  903. onSelect={handlePoSelect}
  904. loading={isPoListLoading}
  905. />
  906. </Stack>
  907. </Grid>
  908. {/* right side po info */}
  909. <Grid item xs={8}>
  910. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
  911. <Grid item xs={12}>
  912. <FormProvider {...dnFormProps}>
  913. <Card sx={{ display: "block" }}>
  914. <CardContent component={Stack} spacing={2}>
  915. <TextField
  916. label={t("Supplier")}
  917. fullWidth
  918. disabled={true}
  919. value={purchaseOrder.supplier ?? ""}
  920. />
  921. <Grid container spacing={2}>
  922. <Grid item xs={6}>
  923. <Stack spacing={2}>
  924. <TextField
  925. label={t("Order Date")}
  926. fullWidth
  927. disabled={true}
  928. value={arrayToDateString(purchaseOrder.orderDate as any)}
  929. />
  930. <TextField
  931. {...dnFormProps.register("dnNo")}
  932. label={t("dnNo")}
  933. type="text"
  934. variant="outlined"
  935. fullWidth
  936. />
  937. </Stack>
  938. </Grid>
  939. <Grid item xs={6}>
  940. <Stack spacing={2}>
  941. <TextField
  942. label={t("ETA")}
  943. fullWidth
  944. disabled={true}
  945. value={arrayToDateString(purchaseOrder.estimatedArrivalDate as any)}
  946. />
  947. <LocalizationProvider
  948. dateAdapter={AdapterDayjs}
  949. adapterLocale="zh-hk"
  950. localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
  951. >
  952. <Controller
  953. control={dnFormProps.control}
  954. name="receiptDate"
  955. render={({ field }) => (
  956. <DatePicker
  957. label={t("receiptDate")}
  958. format={`${OUTPUT_DATE_FORMAT}`}
  959. defaultValue={dateStringToDayjs(field.value)}
  960. onChange={(newValue: Dayjs | null) => {
  961. handleDatePickerChange(newValue, field.onChange);
  962. }}
  963. slotProps={{ textField: { fullWidth: true } }}
  964. />
  965. )}
  966. />
  967. </LocalizationProvider>
  968. </Stack>
  969. </Grid>
  970. </Grid>
  971. </CardContent>
  972. </Card>
  973. </FormProvider>
  974. </Grid>
  975. <Grid item xs={12}>
  976. <Grid container spacing={2} alignItems="stretch">
  977. <Grid item xs={6} sx={{ display: "flex" }}>
  978. <Card sx={{ display: "block", flex: 1 }}>
  979. <CardContent component={Stack} spacing={2}>
  980. <Typography variant="h6">列印</Typography>
  981. <Autocomplete
  982. disableClearable
  983. options={printerCombo}
  984. value={selectedPrinter}
  985. onChange={(_event, value) => setSelectedPrinter(value)}
  986. renderInput={(params) => (
  987. <TextField
  988. {...params}
  989. variant="outlined"
  990. label={t("Printer")}
  991. fullWidth
  992. />
  993. )}
  994. />
  995. <TextField
  996. variant="outlined"
  997. label={t("Print Qty")}
  998. value={printQty}
  999. onChange={(event) => {
  1000. const cleaned = String(event.target.value).replace(/[^0-9]/g, "");
  1001. setPrintQty(Number(cleaned || 0));
  1002. }}
  1003. fullWidth
  1004. />
  1005. <Button
  1006. variant="contained"
  1007. onClick={openPrintDialog}
  1008. disabled={(printerCombo?.length ?? 0) <= 0}
  1009. >
  1010. 選擇列印項目
  1011. </Button>
  1012. <Typography variant="caption" color="text.secondary">
  1013. 只會顯示「待上架 / 已上架」的來貨記錄
  1014. </Typography>
  1015. </CardContent>
  1016. </Card>
  1017. </Grid>
  1018. <Grid item xs={6} sx={{ display: "flex" }}>
  1019. <Card sx={{ display: "block", flex: 1 }}>
  1020. <CardContent component={Stack} spacing={2} sx={{ height: "100%" }}>
  1021. <Typography variant="h6" sx={{ visibility: "hidden" }}>
  1022. 列印
  1023. </Typography>
  1024. <Button
  1025. variant="outlined"
  1026. onClick={fillTodayLotNo}
  1027. sx={{ flex: 1 }}
  1028. >
  1029. 一鍵填入來貨編號(今日)
  1030. </Button>
  1031. </CardContent>
  1032. </Card>
  1033. </Grid>
  1034. </Grid>
  1035. </Grid>
  1036. </Grid>
  1037. </Grid>
  1038. </Grid>
  1039. {/* Area4: Main Table */}
  1040. <Grid container xs={12} justifyContent="start">
  1041. <Grid item xs={12}>
  1042. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  1043. <Table aria-label="collapsible table" stickyHeader>
  1044. <TableHead>
  1045. <TableRow>
  1046. <TableCell align="center" sx={{ width: '60px' }}></TableCell>
  1047. <TableCell sx={{ width: '125px' }}>{t("itemNo")}</TableCell>
  1048. <TableCell align="left" sx={{ width: '125px' }}>{t("itemName")}</TableCell>
  1049. <TableCell align="right">{t("qty")}</TableCell>
  1050. <TableCell align="right">{t("processedQty")}</TableCell>
  1051. <TableCell align="left">{t("uom")}</TableCell>
  1052. <TableCell align="right">{t("receivedTotal")}</TableCell>
  1053. <TableCell align="left">{t("Stock UoM")}</TableCell>
  1054. {/* <TableCell align="right">{t("total weight")}</TableCell> */}
  1055. {/* <TableCell align="right">{`${t("price")} (HKD)`}</TableCell> */}
  1056. <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
  1057. {/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} */}
  1058. <TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
  1059. {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}<br/>(以訂單單位計算)</TableCell> : undefined}
  1060. <TableCell align="center" sx={{ width: '100px' }}></TableCell>
  1061. </TableRow>
  1062. </TableHead>
  1063. <TableBody>
  1064. {rows.map((row) => (
  1065. <Row key={row.id} row={row} />
  1066. ))}
  1067. </TableBody>
  1068. </Table>
  1069. </TableContainer>
  1070. </Grid>
  1071. </Grid>
  1072. {/* area5: selected item info */}
  1073. <Grid container xs={12} justifyContent="start">
  1074. <Grid item xs={12}>
  1075. <Typography variant="h6">
  1076. {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"}
  1077. </Typography>
  1078. </Grid>
  1079. <Grid item xs={12}>
  1080. {selectedRow && (
  1081. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  1082. <Table>
  1083. <TableBody>
  1084. <TableRow>
  1085. <TableCell align="right">
  1086. <Box>
  1087. <PoInputGrid
  1088. // qc={qc}
  1089. setRows={setRows}
  1090. stockInLine={stockInLine}
  1091. setStockInLine={setStockInLine}
  1092. setProcessedQty={setProcessedQty}
  1093. itemDetail={selectedRow}
  1094. warehouse={warehouse}
  1095. fetchPoDetail={fetchPoDetail}
  1096. handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
  1097. printerCombo={printerCombo}
  1098. />
  1099. </Box>
  1100. </TableCell>
  1101. </TableRow>
  1102. </TableBody>
  1103. </Table>
  1104. </TableContainer>
  1105. )}
  1106. </Grid>
  1107. </Grid>
  1108. {/* tab 2 */}
  1109. <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
  1110. {/* <StyledDataGrid
  1111. /> */}
  1112. </Grid>
  1113. </Stack>
  1114. <Dialog open={printDialogOpen} onClose={closePrintDialog} fullWidth maxWidth="md">
  1115. <DialogTitle>列印標籤</DialogTitle>
  1116. <DialogContent>
  1117. <Stack spacing={1.5} sx={{ mt: 1 }}>
  1118. <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
  1119. <FormControlLabel
  1120. control={
  1121. <Checkbox
  1122. checked={printStatusFilter.received}
  1123. onChange={(e) =>
  1124. setPrintStatusFilter((p) => ({ ...p, received: e.target.checked }))
  1125. }
  1126. />
  1127. }
  1128. label="待上架"
  1129. />
  1130. <FormControlLabel
  1131. control={
  1132. <Checkbox
  1133. checked={printStatusFilter.completed}
  1134. onChange={(e) =>
  1135. setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked }))
  1136. }
  1137. />
  1138. }
  1139. label="已上架"
  1140. />
  1141. <FormControlLabel
  1142. control={
  1143. <Checkbox
  1144. checked={
  1145. eligiblePrintSils.length > 0 &&
  1146. selectedPrintSilIds.size === eligiblePrintSils.length
  1147. }
  1148. indeterminate={
  1149. selectedPrintSilIds.size > 0 &&
  1150. selectedPrintSilIds.size < eligiblePrintSils.length
  1151. }
  1152. onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)}
  1153. />
  1154. }
  1155. label="全選(目前篩選結果)"
  1156. />
  1157. <Typography variant="caption" color="text.secondary">
  1158. 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length}
  1159. </Typography>
  1160. </Stack>
  1161. <TableContainer component={Paper} variant="outlined">
  1162. <Table size="small" stickyHeader>
  1163. <TableHead>
  1164. <TableRow>
  1165. <TableCell padding="checkbox"></TableCell>
  1166. <TableCell>貨品編號</TableCell>
  1167. <TableCell>貨品名稱</TableCell>
  1168. <TableCell align="right">換算庫存數量</TableCell>
  1169. <TableCell>庫存單位</TableCell>
  1170. <TableCell>收貨日期</TableCell>
  1171. <TableCell>來貨批號</TableCell>
  1172. <TableCell>來貨狀態</TableCell>
  1173. </TableRow>
  1174. </TableHead>
  1175. <TableBody>
  1176. {eligiblePrintSils.map((sil) => {
  1177. const status = (sil.status ?? "").toLowerCase().trim();
  1178. const statusText =
  1179. status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status;
  1180. const receiptText = sil.receiptDate
  1181. ? Array.isArray(sil.receiptDate)
  1182. ? arrayToDateString(sil.receiptDate)
  1183. : String(sil.receiptDate)
  1184. : "-";
  1185. const stockQty = Number(sil.acceptedQty ?? 0);
  1186. const stockQtyText =
  1187. Number.isFinite(stockQty) && stockQty > 0
  1188. ? decimalFormatter.format(stockQty)
  1189. : decimalFormatter.format(0);
  1190. return (
  1191. <TableRow key={sil.id} hover>
  1192. <TableCell padding="checkbox">
  1193. <Checkbox
  1194. checked={selectedPrintSilIds.has(sil.id)}
  1195. onChange={(e) => togglePrintSilSelection(sil.id, e.target.checked)}
  1196. />
  1197. </TableCell>
  1198. <TableCell>{sil.itemNo}</TableCell>
  1199. <TableCell>{sil.itemName}</TableCell>
  1200. <TableCell align="right">{stockQtyText}</TableCell>
  1201. <TableCell>{sil.stockUomDesc || "-"}</TableCell>
  1202. <TableCell>{receiptText}</TableCell>
  1203. <TableCell>{sil.productLotNo || "-"}</TableCell>
  1204. <TableCell>{statusText}</TableCell>
  1205. </TableRow>
  1206. );
  1207. })}
  1208. {eligiblePrintSils.length === 0 && (
  1209. <TableRow>
  1210. <TableCell colSpan={8}>
  1211. <Typography variant="body2" color="text.secondary">
  1212. 沒有符合條件的項目
  1213. </Typography>
  1214. </TableCell>
  1215. </TableRow>
  1216. )}
  1217. </TableBody>
  1218. </Table>
  1219. </TableContainer>
  1220. </Stack>
  1221. </DialogContent>
  1222. <DialogActions>
  1223. <Button onClick={closePrintDialog} disabled={isBulkPrinting}>
  1224. 取消
  1225. </Button>
  1226. <Button
  1227. variant="contained"
  1228. onClick={handleBulkPrint}
  1229. disabled={isBulkPrinting || selectedPrintSilIds.size <= 0 || !selectedPrinter}
  1230. >
  1231. {isBulkPrinting ? "列印中..." : "列印"}
  1232. </Button>
  1233. </DialogActions>
  1234. </Dialog>
  1235. {/* {itemInfo !== undefined && (
  1236. <>
  1237. <PoQcStockInModal
  1238. type={"putaway"}
  1239. open={putAwayOpen}
  1240. warehouse={warehouse}
  1241. setItemDetail={setItemInfo}
  1242. onClose={closePutAwayModal}
  1243. itemDetail={itemInfo}
  1244. />
  1245. </>
  1246. )} */}
  1247. </>
  1248. );
  1249. };
  1250. export default PoDetail;