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

ProductionProcessPage.tsx 10 KiB

5ヶ月前
3週間前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3週間前
3週間前
5ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4週間前
5ヶ月前
4週間前
3週間前
2ヶ月前
2ヶ月前
4週間前
4週間前
4週間前
5ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. "use client";
  2. import React, { useState, useEffect, useCallback } from "react";
  3. import { useSession } from "next-auth/react";
  4. import { SessionWithTokens } from "@/config/authConfig";
  5. import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material";
  6. import { usePathname, useRouter, useSearchParams } from "next/navigation";
  7. import QcStockInModal from "@/components/Qc/QcStockInModal";
  8. import ProductionProcessList, {
  9. createDefaultProductionProcessListPersistedState,
  10. } from "@/components/ProductionProcess/ProductionProcessList";
  11. import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail";
  12. import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail";
  13. import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan";
  14. import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus";
  15. import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard";
  16. import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard";
  17. import type { PrinterCombo } from "@/app/api/settings/printer";
  18. import { useTranslation } from "react-i18next";
  19. interface ProductionProcessPageProps {
  20. printerCombo: PrinterCombo[];
  21. }
  22. const STORAGE_KEY = 'productionProcess_selectedMatchingStock';
  23. const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => {
  24. const { t } = useTranslation(["common"]);
  25. const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
  26. const [selectedMatchingStock, setSelectedMatchingStock] = useState<{
  27. jobOrderId: number;
  28. productProcessId: number;
  29. pickOrderId: number;
  30. } | null>(null);
  31. const [tabIndex, setTabIndex] = useState(0);
  32. /** 列表搜尋/分頁:保留在切換工單詳情時,返回後仍為同一條件 */
  33. const [productionListState, setProductionListState] = useState(
  34. createDefaultProductionProcessListPersistedState,
  35. );
  36. const [waitingPutawayListState, setWaitingPutawayListState] = useState(
  37. createDefaultProductionProcessListPersistedState,
  38. );
  39. const [putawayedListState, setPutawayedListState] = useState(
  40. createDefaultProductionProcessListPersistedState,
  41. );
  42. const { data: session } = useSession() as { data: SessionWithTokens | null };
  43. const sessionToken = session as SessionWithTokens | null;
  44. const searchParams = useSearchParams();
  45. const pathname = usePathname();
  46. const router = useRouter();
  47. const [linkQcOpen, setLinkQcOpen] = useState(false);
  48. const [linkQcSilId, setLinkQcSilId] = useState<number | null>(null);
  49. // Add printer selection state
  50. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
  51. printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
  52. );
  53. // 从 sessionStorage 恢复状态(仅在客户端)
  54. useEffect(() => {
  55. if (typeof window !== 'undefined') {
  56. try {
  57. const saved = sessionStorage.getItem(STORAGE_KEY);
  58. if (saved) {
  59. const parsed = JSON.parse(saved);
  60. // 验证数据有效性
  61. if (parsed && typeof parsed.jobOrderId === 'number' && typeof parsed.productProcessId === 'number') {
  62. setSelectedMatchingStock(parsed);
  63. console.log(" Restored selectedMatchingStock from sessionStorage:", parsed);
  64. }
  65. }
  66. } catch (error) {
  67. console.error("Error restoring selectedMatchingStock:", error);
  68. sessionStorage.removeItem(STORAGE_KEY);
  69. }
  70. }
  71. }, []);
  72. // 保存状态到 sessionStorage
  73. useEffect(() => {
  74. if (typeof window !== 'undefined') {
  75. if (selectedMatchingStock) {
  76. sessionStorage.setItem(STORAGE_KEY, JSON.stringify(selectedMatchingStock));
  77. console.log(" Saved selectedMatchingStock to sessionStorage:", selectedMatchingStock);
  78. } else {
  79. sessionStorage.removeItem(STORAGE_KEY);
  80. }
  81. }
  82. }, [selectedMatchingStock]);
  83. // 处理返回列表时清除存储
  84. const handleBackFromSecondScan = useCallback(() => {
  85. setSelectedMatchingStock(null);
  86. if (typeof window !== 'undefined') {
  87. sessionStorage.removeItem(STORAGE_KEY);
  88. }
  89. }, []);
  90. const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => {
  91. setTabIndex(newValue);
  92. }, []);
  93. const openStockInLineIdQ = searchParams.get("openStockInLineId");
  94. /** Deep link from nav alert: /productionProcess?openStockInLineId=… → 「完成QC工單」tab + FG QC modal */
  95. useEffect(() => {
  96. if (!openStockInLineIdQ) {
  97. setLinkQcOpen(false);
  98. setLinkQcSilId(null);
  99. return;
  100. }
  101. const id = parseInt(openStockInLineIdQ, 10);
  102. if (!Number.isFinite(id) || id <= 0) return;
  103. setSelectedProcessId(null);
  104. setSelectedMatchingStock(null);
  105. setTabIndex(1);
  106. setLinkQcSilId(id);
  107. setLinkQcOpen(true);
  108. }, [openStockInLineIdQ]);
  109. const closeLinkQc = useCallback(() => {
  110. setLinkQcOpen(false);
  111. setLinkQcSilId(null);
  112. const p = new URLSearchParams(searchParams.toString());
  113. p.delete("openStockInLineId");
  114. const q = p.toString();
  115. router.replace(q ? `${pathname}?${q}` : pathname, { scroll: false });
  116. }, [pathname, router, searchParams]);
  117. if (selectedMatchingStock) {
  118. return (
  119. <JobPickExecutionsecondscan
  120. filterArgs={{
  121. jobOrderId: selectedMatchingStock.jobOrderId,
  122. pickOrderId: selectedMatchingStock.pickOrderId,
  123. }}
  124. onBack={handleBackFromSecondScan}
  125. />
  126. );
  127. }
  128. if (selectedProcessId !== null) {
  129. return (
  130. <ProductionProcessJobOrderDetail
  131. jobOrderId={selectedProcessId}
  132. initialTabIndex={2}
  133. onBack={() => setSelectedProcessId(null)}
  134. />
  135. );
  136. }
  137. return (
  138. <>
  139. <Box>
  140. {/* Header section with printer selection */}
  141. {tabIndex === 1 && (
  142. <Box sx={{
  143. p: 1,
  144. borderBottom: '1px solid #e0e0e0',
  145. minHeight: 'auto',
  146. display: 'flex',
  147. alignItems: 'center',
  148. justifyContent: 'flex-end',
  149. gap: 2,
  150. flexWrap: 'wrap',
  151. }}>
  152. <Stack
  153. direction="row"
  154. spacing={2}
  155. sx={{
  156. alignItems: 'center',
  157. flexWrap: 'wrap',
  158. rowGap: 1,
  159. }}
  160. >
  161. <Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}>
  162. {t("Select Printer")}:
  163. </Typography>
  164. <Autocomplete
  165. disableClearable
  166. options={printerCombo || []}
  167. getOptionLabel={(option) =>
  168. option.name || option.label || option.code || `Printer ${option.id}`
  169. }
  170. value={selectedPrinter || undefined}
  171. onChange={(_, newValue) => setSelectedPrinter(newValue)}
  172. sx={{ minWidth: 200 }}
  173. size="small"
  174. renderInput={(params) => (
  175. <TextField
  176. {...params}
  177. placeholder={t("Printer")}
  178. inputProps={{
  179. ...params.inputProps,
  180. readOnly: true,
  181. }}
  182. />
  183. )}
  184. />
  185. </Stack>
  186. </Box>
  187. )}
  188. <Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}>
  189. <Tab label={t("Production Process")} />
  190. <Tab label={t("Waiting QC Put Away Job Orders")} />
  191. <Tab label={t("Put Awayed Job Orders")} />
  192. <Tab label={t("Job Process Status Dashboard")} />
  193. <Tab label={t("Operator KPI Dashboard")} />
  194. <Tab label={t("Production Equipment Status Dashboard")} />
  195. </Tabs>
  196. {tabIndex === 0 && (
  197. <ProductionProcessList
  198. printerCombo={printerCombo}
  199. qcReady={false}
  200. disableDateFilter={true}
  201. listPersistedState={productionListState}
  202. onListPersistedStateChange={setProductionListState}
  203. onSelectProcess={(jobOrderId) => {
  204. const id = jobOrderId ?? null;
  205. if (id !== null) {
  206. setSelectedProcessId(id);
  207. }
  208. }}
  209. onSelectMatchingStock={(jobOrderId, productProcessId,pickOrderId) => {
  210. setSelectedMatchingStock({
  211. jobOrderId: jobOrderId || 0,
  212. productProcessId: productProcessId || 0 ,
  213. pickOrderId: pickOrderId || 0
  214. });
  215. }}
  216. />
  217. )}
  218. {tabIndex === 1 && (
  219. <ProductionProcessList
  220. printerCombo={printerCombo}
  221. qcReady={true}
  222. includePutaway={true}
  223. putawayStatus="notCompleted"
  224. listPersistedState={waitingPutawayListState}
  225. onListPersistedStateChange={setWaitingPutawayListState}
  226. onSelectProcess={(jobOrderId) => {
  227. const id = jobOrderId ?? null;
  228. if (id !== null) {
  229. setSelectedProcessId(id);
  230. }
  231. }}
  232. onSelectMatchingStock={(jobOrderId, productProcessId, pickOrderId) => {
  233. setSelectedMatchingStock({
  234. jobOrderId: jobOrderId || 0,
  235. productProcessId: productProcessId || 0,
  236. pickOrderId: pickOrderId || 0,
  237. });
  238. }}
  239. />
  240. )}
  241. {tabIndex === 2 && (
  242. <ProductionProcessList
  243. printerCombo={printerCombo}
  244. qcReady={true}
  245. includePutaway={true}
  246. putawayStatus="completed"
  247. listPersistedState={putawayedListState}
  248. onListPersistedStateChange={setPutawayedListState}
  249. onSelectProcess={(jobOrderId) => {
  250. const id = jobOrderId ?? null;
  251. if (id !== null) {
  252. setSelectedProcessId(id);
  253. }
  254. }}
  255. onSelectMatchingStock={(jobOrderId, productProcessId, pickOrderId) => {
  256. setSelectedMatchingStock({
  257. jobOrderId: jobOrderId || 0,
  258. productProcessId: productProcessId || 0,
  259. pickOrderId: pickOrderId || 0,
  260. });
  261. }}
  262. />
  263. )}
  264. {tabIndex === 3 && (
  265. <JobProcessStatus />
  266. )}
  267. {tabIndex === 4 && (
  268. <OperatorKpiDashboard />
  269. )}
  270. {tabIndex === 5 && (
  271. <EquipmentStatusDashboard />
  272. )}
  273. </Box>
  274. <QcStockInModal
  275. session={sessionToken}
  276. open={Boolean(linkQcOpen && linkQcSilId != null && linkQcSilId > 0)}
  277. onClose={closeLinkQc}
  278. inputDetail={linkQcSilId != null && linkQcSilId > 0 ? { id: linkQcSilId } : undefined}
  279. printerCombo={printerCombo}
  280. warehouse={[]}
  281. printSource="productionProcess"
  282. uiMode="default"
  283. />
  284. </>
  285. );
  286. };
  287. export default ProductionProcessPage;