| @@ -200,6 +200,21 @@ export const fetchPoInClient = cache(async (id: number) => { | |||||
| }); | }); | ||||
| }); | }); | ||||
| export interface PurchaseOrderSummary { | |||||
| id: number; | |||||
| code: string; | |||||
| status: string; | |||||
| orderDate: string; | |||||
| estimatedArrivalDate: string; | |||||
| supplierName: string; | |||||
| escalated: boolean; | |||||
| } | |||||
| export const fetchPoSummariesClient = cache(async (ids: number[]) => { | |||||
| return serverFetchJson<PurchaseOrderSummary[]>(`${BASE_API_URL}/po/summary`, { | |||||
| next: { tags: ["po"] }, | |||||
| }); | |||||
| }); | |||||
| export const fetchPoListClient = cache( | export const fetchPoListClient = cache( | ||||
| async (queryParams?: Record<string, any>) => { | async (queryParams?: Record<string, any>) => { | ||||
| if (queryParams) { | if (queryParams) { | ||||
| @@ -43,6 +43,7 @@ import { | |||||
| checkPolAndCompletePo, | checkPolAndCompletePo, | ||||
| fetchPoInClient, | fetchPoInClient, | ||||
| fetchPoListClient, | fetchPoListClient, | ||||
| fetchPoSummariesClient, | |||||
| startPo, | startPo, | ||||
| } from "@/app/api/po/actions"; | } from "@/app/api/po/actions"; | ||||
| import { | import { | ||||
| @@ -201,6 +202,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| purchaseOrder.pol || [], | purchaseOrder.pol || [], | ||||
| ); | ); | ||||
| const [polInputList, setPolInputList] = useState<PolInputResult[]>([]) | const [polInputList, setPolInputList] = useState<PolInputResult[]>([]) | ||||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setPolInputList( | setPolInputList( | ||||
| (purchaseOrder.pol ?? []).map(() => ({ | (purchaseOrder.pol ?? []).map(() => ({ | ||||
| @@ -209,7 +211,21 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| } as PolInputResult)) | } as PolInputResult)) | ||||
| ); | ); | ||||
| }, [purchaseOrder.pol]); | }, [purchaseOrder.pol]); | ||||
| useEffect(() => { | |||||
| try { | |||||
| const raw = sessionStorage.getItem("po-detail-selection"); | |||||
| if (raw) { | |||||
| const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[]; | |||||
| if (Array.isArray(parsed) && parsed.length > 0) { | |||||
| setPoList(parsed as PoResult[]); | |||||
| sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据 | |||||
| } | |||||
| } | |||||
| } catch (e) { | |||||
| console.warn("sessionStorage getItem/parse failed", e); | |||||
| } | |||||
| }, []); | |||||
| const pathname = usePathname() | const pathname = usePathname() | ||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| @@ -227,6 +243,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [poList, setPoList] = useState<PoResult[]>([]); | const [poList, setPoList] = useState<PoResult[]>([]); | ||||
| const [isPoListLoading, setIsPoListLoading] = useState(false); | |||||
| const [selectedPoId, setSelectedPoId] = useState(po.id); | const [selectedPoId, setSelectedPoId] = useState(po.id); | ||||
| const [focusField, setFocusField] = useState<HTMLInputElement>(); | const [focusField, setFocusField] = useState<HTMLInputElement>(); | ||||
| @@ -240,15 +257,26 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| } | } | ||||
| }) | }) | ||||
| const fetchPoList = useCallback(async () => { | const fetchPoList = useCallback(async () => { | ||||
| setIsPoListLoading(true); | |||||
| try { | try { | ||||
| if (selectedIdsParam) { | if (selectedIdsParam) { | ||||
| const selectedIds = selectedIdsParam.split(',').map(id => parseInt(id)); | |||||
| const promises = selectedIds.map(id => fetchPoInClient(id)); | |||||
| const results = await Promise.all(promises); | |||||
| setPoList(results.filter(Boolean)); | |||||
| const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死 | |||||
| const allIds = selectedIdsParam | |||||
| .split(',') | |||||
| .map(id => parseInt(id)) | |||||
| .filter(id => !Number.isNaN(id)); | |||||
| const limitedIds = allIds.slice(0, MAX_IDS); | |||||
| if (allIds.length > MAX_IDS) { | |||||
| console.warn( | |||||
| `selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.` | |||||
| ); | |||||
| } | |||||
| const result = await fetchPoSummariesClient(limitedIds); | |||||
| setPoList(result as any); | |||||
| } else { | } else { | ||||
| const result = await fetchPoListClient({ limit: 20, offset: 0 }); | const result = await fetchPoListClient({ limit: 20, offset: 0 }); | ||||
| if (result && result.records) { | if (result && result.records) { | ||||
| setPoList(result.records); | setPoList(result.records); | ||||
| @@ -256,6 +284,8 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to fetch PO list:", error); | console.error("Failed to fetch PO list:", error); | ||||
| } finally { | |||||
| setIsPoListLoading(false); | |||||
| } | } | ||||
| }, [selectedIdsParam]); | }, [selectedIdsParam]); | ||||
| @@ -311,11 +341,11 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| fetchPoDetail(currentPoId); | fetchPoDetail(currentPoId); | ||||
| } | } | ||||
| }, [currentPoId, fetchPoDetail]); | }, [currentPoId, fetchPoDetail]); | ||||
| /* | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchPoList(); | fetchPoList(); | ||||
| }, [fetchPoList]); | }, [fetchPoList]); | ||||
| */ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (currentPoId) { | if (currentPoId) { | ||||
| setSelectedPoId(parseInt(currentPoId)); | setSelectedPoId(parseInt(currentPoId)); | ||||
| @@ -490,12 +520,15 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| const highlightColor = (Number(receivedTotal.replace(/,/g, '')) <= 0) ? "red" : "inherit"; | const highlightColor = (Number(receivedTotal.replace(/,/g, '')) <= 0) ? "red" : "inherit"; | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <TableRow | <TableRow | ||||
| sx={{ "& > *": { borderBottom: "unset" }, | sx={{ "& > *": { borderBottom: "unset" }, | ||||
| color: "black" | color: "black" | ||||
| }} | }} | ||||
| onClick={() => changeStockInLines(row.id)} | onClick={() => changeStockInLines(row.id)} | ||||
| > | > | ||||
| {/* <TableCell> | {/* <TableCell> | ||||
| <IconButton | <IconButton | ||||
| disabled={purchaseOrder.status.toLowerCase() === "pending"} | disabled={purchaseOrder.status.toLowerCase() === "pending"} | ||||
| @@ -729,13 +762,15 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}> | <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}> | ||||
| {/* left side select po */} | {/* left side select po */} | ||||
| <Grid item xs={4}> | <Grid item xs={4}> | ||||
| <PoSearchList | |||||
| poList={poList} | |||||
| selectedPoId={selectedPoId} | |||||
| onSelect={handlePoSelect} | |||||
| /> | |||||
| </Grid> | |||||
| <Stack spacing={1}> | |||||
| <PoSearchList | |||||
| poList={poList} | |||||
| selectedPoId={selectedPoId} | |||||
| onSelect={handlePoSelect} | |||||
| /> | |||||
| </Stack> | |||||
| </Grid> | |||||
| {/* right side po info */} | {/* right side po info */} | ||||
| <Grid item xs={8}> | <Grid item xs={8}> | ||||
| @@ -41,6 +41,7 @@ const PoSearch: React.FC<Props> = ({ | |||||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({estimatedArrivalDate : dayjsToDateString(dayjs(), "input")}); | const [filterArgs, setFilterArgs] = useState<Record<string, any>>({estimatedArrivalDate : dayjsToDateString(dayjs(), "input")}); | ||||
| const { t } = useTranslation(["purchaseOrder", "dashboard"]); | const { t } = useTranslation(["purchaseOrder", "dashboard"]); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | |||||
| const [pagingController, setPagingController] = useState( | const [pagingController, setPagingController] = useState( | ||||
| defaultPagingController, | defaultPagingController, | ||||
| ); | ); | ||||
| @@ -83,7 +84,18 @@ const PoSearch: React.FC<Props> = ({ | |||||
| (po: PoResult) => { | (po: PoResult) => { | ||||
| setSelectedPoIds([]); | setSelectedPoIds([]); | ||||
| setSelectAll(false); | setSelectAll(false); | ||||
| router.push(`/po/edit?id=${po.id}&start=true&selectedIds=${po.id}`); | |||||
| const listForDetail = [ | |||||
| { id: po.id, code: po.code, status: po.status, supplier: po.supplier ?? null }, | |||||
| ]; | |||||
| try { | |||||
| sessionStorage.setItem( | |||||
| PO_DETAIL_SELECTION_KEY, | |||||
| JSON.stringify(listForDetail), | |||||
| ); | |||||
| } catch (e) { | |||||
| console.warn("sessionStorage setItem failed", e); | |||||
| } | |||||
| router.push(`/po/edit?id=${po.id}&start=true`); | |||||
| }, | }, | ||||
| [router], | [router], | ||||
| ); | ); | ||||
| @@ -111,12 +123,26 @@ const PoSearch: React.FC<Props> = ({ | |||||
| // navigate to PoDetail page | // navigate to PoDetail page | ||||
| const handleGoToPoDetail = useCallback(() => { | const handleGoToPoDetail = useCallback(() => { | ||||
| if (selectedPoIds.length > 0) { | |||||
| const selectedIdsParam = selectedPoIds.join(','); | |||||
| const firstPoId = selectedPoIds[0]; | |||||
| router.push(`/po/edit?id=${firstPoId}&start=true&selectedIds=${selectedIdsParam}`); | |||||
| if (selectedPoIds.length === 0) return; | |||||
| const selectedList = filteredPo.filter((p) => selectedPoIds.includes(p.id)); | |||||
| const listForDetail = selectedList.map((p) => ({ | |||||
| id: p.id, | |||||
| code: p.code, | |||||
| status: p.status, | |||||
| supplier: p.supplier ?? null, | |||||
| })); | |||||
| try { | |||||
| sessionStorage.setItem("po-detail-selection", JSON.stringify(listForDetail)); | |||||
| } catch (e) { | |||||
| console.warn("sessionStorage setItem failed", e); | |||||
| } | } | ||||
| }, [selectedPoIds, router]); | |||||
| const selectedIdsParam = selectedPoIds.join(","); | |||||
| const firstPoId = selectedPoIds[0]; | |||||
| router.push(`/po/edit?id=${firstPoId}&start=true&selectedIds=${selectedIdsParam}`); | |||||
| }, [selectedPoIds, filteredPo, router]); | |||||
| const itemColumn = useCallback((value: string | undefined) => { | const itemColumn = useCallback((value: string | undefined) => { | ||||
| if (!value) { | if (!value) { | ||||
| @@ -11,6 +11,7 @@ | |||||
| "Variance %": "差異百分比", | "Variance %": "差異百分比", | ||||
| "fg": "成品", | "fg": "成品", | ||||
| "Back to List": "返回列表", | "Back to List": "返回列表", | ||||
| "Start Stock Take Date": "盤點日期", | |||||
| "Record Status": "記錄狀態", | "Record Status": "記錄狀態", | ||||
| "Stock take record status updated to not match": "盤點記錄狀態更新為數值不符", | "Stock take record status updated to not match": "盤點記錄狀態更新為數值不符", | ||||
| "available": "可用", | "available": "可用", | ||||
| @@ -9,6 +9,7 @@ | |||||
| "Edit Product / Material": "編輯產品 / 材料", | "Edit Product / Material": "編輯產品 / 材料", | ||||
| "Product / Material": "產品 / 材料", | "Product / Material": "產品 / 材料", | ||||
| "Product / Material Details": "產品 / 材料詳情", | "Product / Material Details": "產品 / 材料詳情", | ||||
| "Qc items": "QC 項目", | "Qc items": "QC 項目", | ||||
| "Qc Category": "質檢模板", | "Qc Category": "質檢模板", | ||||
| "Name": "名稱", | "Name": "名稱", | ||||
| @@ -29,12 +29,12 @@ | |||||
| "Do you want to start?": "確定開始嗎?", | "Do you want to start?": "確定開始嗎?", | ||||
| "Start": "開始", | "Start": "開始", | ||||
| "Pick Order Code(s)": "提料單編號", | "Pick Order Code(s)": "提料單編號", | ||||
| "Delivery Order Code(s)": "送貨單編號", | |||||
| "Delivery Order Code(s)": "提料單編號", | |||||
| "Start Success": "開始成功", | "Start Success": "開始成功", | ||||
| "Truck Lance Code": "車牌號碼", | "Truck Lance Code": "車牌號碼", | ||||
| "Pick Order Codes": "提料單編號", | "Pick Order Codes": "提料單編號", | ||||
| "Pick Order Lines": "提料單行數", | "Pick Order Lines": "提料單行數", | ||||
| "Delivery Order Codes": "送貨單編號", | |||||
| "Delivery Order Codes": "提料單編號", | |||||
| "Delivery Order Lines": "送貨單行數", | "Delivery Order Lines": "送貨單行數", | ||||
| "Lines Per Pick Order": "每提料單行數", | "Lines Per Pick Order": "每提料單行數", | ||||
| "Pick Orders Details": "提料單詳情", | "Pick Orders Details": "提料單詳情", | ||||
| @@ -394,9 +394,15 @@ | |||||
| "submitStockIn": "提交入庫", | "submitStockIn": "提交入庫", | ||||
| "not default warehosue": "不是默認倉庫", | "not default warehosue": "不是默認倉庫", | ||||
| "printQty": "打印數量", | "printQty": "打印數量", | ||||
| "Shop": "商店名稱", | |||||
| "warehouse": "倉庫", | "warehouse": "倉庫", | ||||
| "Add Record": "添加記錄", | "Add Record": "添加記錄", | ||||
| "Clean Record": "清空記錄", | "Clean Record": "清空記錄", | ||||
| "Select": "選擇", | |||||
| "Close": "關閉", | |||||
| "Truck": "車輛", | |||||
| "Date": "日期", | |||||
| "Delivery Order Code": "送貨單編號", | |||||
| "Escalation Info": "升級信息", | "Escalation Info": "升級信息", | ||||
| "Escalation Result": "升級結果", | "Escalation Result": "升級結果", | ||||
| "acceptQty must not greater than": "接受數量不能大於", | "acceptQty must not greater than": "接受數量不能大於", | ||||
| @@ -426,10 +432,17 @@ | |||||
| "No entries available": "該樓層未有需處理訂單", | "No entries available": "該樓層未有需處理訂單", | ||||
| "Today": "是日", | "Today": "是日", | ||||
| "Tomorrow": "翌日", | "Tomorrow": "翌日", | ||||
| "No Stock Available": "沒有庫存可用", | |||||
| "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", | |||||
| "Day After Tomorrow": "後日", | "Day After Tomorrow": "後日", | ||||
| "Select Date": "請選擇日期", | "Select Date": "請選擇日期", | ||||
| "Search by Shop": "搜尋商店", | |||||
| "Search by Truck": "搜尋貨車", | |||||
| "Print DN & Label": "列印提料單和送貨單標籤", | "Print DN & Label": "列印提料單和送貨單標籤", | ||||
| "Print Label": "列印送貨單標籤", | "Print Label": "列印送貨單標籤", | ||||
| "Not Yet Finished Released Do Pick Orders": "未完成提料單", | |||||
| "Not yet finished released do pick orders": "未完成提料單", | |||||
| "Released orders not yet completed - click lane to select and assign": "未完成提料單- 點擊貨車班次選擇並分配", | |||||
| "Ticket Release Table": "查看提貨情況", | "Ticket Release Table": "查看提貨情況", | ||||
| "Please take one pick order before printing the draft.": "請先從「撳單/提料單詳情」頁面下方選取提料單,再列印草稿。", | "Please take one pick order before printing the draft.": "請先從「撳單/提料單詳情」頁面下方選取提料單,再列印草稿。", | ||||
| "No released pick order records found.": "目前沒有可用的提料單。", | "No released pick order records found.": "目前沒有可用的提料單。", | ||||